javaandroidandroid-animationobjectanimatorviewpropertyanimator

Android - Zoom animation using AnimatorSet


The official Zooming a View tutorial uses an AnimatorSet to zoom into a View. It creates the illusion of downward movement as the view expands. Later, the AnimatorSet is simply replayed backwards to create the illusion of zoom-out.

zoom-in with downward movement What I need to implement is the exact reverse of this. I need to start with an expanded view and shrink it into a smaller view with an upward movement:

zoom-out with upward movement It doesn't seem that I can use the reversal code in the example. That example assumes that you first zoom into the view and expand it, and then shrink it back into the original thumbnail icon.

Here's what I've tried so far. My XML layout is

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#1999da">             

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:orientation="horizontal"
        android:layout_gravity="center"
        android:gravity="center">

        <!-- The final shrunk image -->

        <ImageView
            android:id="@+id/thumb_button_1"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:layout_marginRight="1dp"
            android:visibility="invisible"/>

    </LinearLayout>

</LinearLayout>

<!-- The initial expanded image that needs to be shrunk -->

<ImageView
    android:id="@+id/expanded_image"
    android:layout_width="wrap_content"
    android:layout_height="125dp"
    android:layout_gravity="center"
    android:src="@drawable/title_logo_expanded"
    android:scaleType="centerCrop"/>

</FrameLayout>

And here is the method that performs the zoom-out operation. I've basically tried to reverse the procedure in the tutorial:

private void zoomImageFromThumbReverse(final View expandedImageView, int imageResId, final int duration) {
    // If there's an animation in progress, cancel it immediately and proceed with this one.      

    if (mCurrentAnimator != null) {
        mCurrentAnimator.cancel();
    }

    // Load the low-resolution "zoomed-out" image.
    final ImageView thumbView = (ImageView) findViewById(R.id.thumb_button_1);
    thumbView.setImageResource(imageResId);

    // Calculate the starting and ending bounds for the zoomed-in image. This step
    // involves lots of math. Yay, math.
    final Rect startBounds = new Rect();
    final Rect finalBounds = new Rect();
    final Point globalOffset = new Point();

    // The start bounds are the global visible rectangle of the container view (i.e. the FrameLayout), and the
    // final bounds are the global visible rectangle of the thumbnail. Also
    // set the container view's offset as the origin for the bounds, since that's
    // the origin for the positioning animation properties (X, Y).
    findViewById(R.id.container).getGlobalVisibleRect(startBounds, globalOffset);
    thumbView.getGlobalVisibleRect(finalBounds);
    startBounds.offset(-globalOffset.x, -globalOffset.y);
    finalBounds.offset(-globalOffset.x, -globalOffset.y);

    // Adjust the start bounds to be the same aspect ratio as the final bounds using the
    // "center crop" technique. This prevents undesirable stretching during the animation.
    // Also calculate the start scaling factor (the end scaling factor is always 1.0).
    float startScale;
    if ((float) finalBounds.width() / finalBounds.height()
            > (float) startBounds.width() / startBounds.height()) {
        // Extend start bounds horizontally
        startScale = (float) startBounds.height() / finalBounds.height();
        float startWidth = startScale * finalBounds.width();
        float deltaWidth = (startWidth - startBounds.width()) / 2;
        startBounds.left -= deltaWidth;
        startBounds.right += deltaWidth;
    } else {
        // Extend start bounds vertically
        startScale = (float) startBounds.width() / finalBounds.width();
        float startHeight = startScale * finalBounds.height();
        float deltaHeight = (startHeight - startBounds.height()) / 2;
        startBounds.top -= deltaHeight;
        startBounds.bottom += deltaHeight;
    }

    // Hide the expanded-image and show the zoomed-out, thumbnail view. When the animation begins,
    // it will position the zoomed-in view in the place of the thumbnail.
    expandedImageView.setAlpha(0f);
    thumbView.setVisibility(View.VISIBLE);

    // Set the pivot point for SCALE_X and SCALE_Y transformations to the top-left corner of
    // the zoomed-in view (the default is the center of the view).
    thumbView.setPivotX(0f);
    thumbView.setPivotY(0f);

    // Construct and run the parallel animation of the four translation and scale properties
    // (X, Y, SCALE_X, and SCALE_Y).
    AnimatorSet set = new AnimatorSet();
    set
            .play(ObjectAnimator.ofFloat(thumbView, View.X, startBounds.left,
                    finalBounds.left))
            .with(ObjectAnimator.ofFloat(thumbView, View.Y, startBounds.top,
                    finalBounds.top))
            .with(ObjectAnimator.ofFloat(thumbView, View.SCALE_X, startScale, 1f))
            .with(ObjectAnimator.ofFloat(thumbView, View.SCALE_Y, startScale, 1f));
    //set.setDuration(mShortAnimationDuration);
    set.setDuration(duration);
    set.setInterpolator(new DecelerateInterpolator());
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mCurrentAnimator = null;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            mCurrentAnimator = null;
        }
    });
    set.start();
    mCurrentAnimator = set;

    // Upon clicking the zoomed-out image, it should zoom back down to the original bounds
    // and show the thumbnail instead of the expanded image.
    final float startScaleFinal = startScale;
    thumbView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mCurrentAnimator != null) {
                mCurrentAnimator.cancel();
            }

            // Animate the four positioning/sizing properties in parallel, back to their
            // original values.
            AnimatorSet set = new AnimatorSet();
            set
                    .play(ObjectAnimator.ofFloat(thumbView, View.X, startBounds.left))
                    .with(ObjectAnimator.ofFloat(thumbView, View.Y, startBounds.top))
                    .with(ObjectAnimator
                            .ofFloat(thumbView, View.SCALE_X, startScaleFinal))
                    .with(ObjectAnimator
                            .ofFloat(thumbView, View.SCALE_Y, startScaleFinal));
            //set.setDuration(mShortAnimationDuration);
            set.setDuration(duration);
            set.setInterpolator(new DecelerateInterpolator());
            set.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    expandedImageView.setAlpha(1f);
                    thumbView.setVisibility(View.GONE);
                    mCurrentAnimator = null;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    expandedImageView.setAlpha(1f);
                    thumbView.setVisibility(View.GONE);
                    mCurrentAnimator = null;
                }
            });
            set.start();
            mCurrentAnimator = set;
        }
    });
}

I am invoking this method in onCreate() as follows:

final View expandedImageView = findViewById(R.id.expanded_image);
new Handler().postDelayed(new Runnable(){
        public void run() {
            zoomImageFromThumbReverse(expandedImageView, R.drawable.title_logo_min, 1000);
        }}, 1000);

Well, that's it, folks. It isn't working. I am at a loss as to why. The demo example works perfectly, so why doesn't this work ? Take a gander and tell me if I'm crazy.

Can anyone identify the error ? Or point me in the right direction ? All help will be greatly appreciated.


Solution

  • This is the solution I have ultimately used:

    private void applyAnimation(final View startView, final View finishView, long duration) {
        float scalingFactor = ((float)finishView.getHeight())/((float)startView.getHeight());
    
        ScaleAnimation scaleAnimation =  new ScaleAnimation(1f, scalingFactor,
                                                            1f, scalingFactor,
                                                            Animation.RELATIVE_TO_SELF, 0.5f,
                                                            Animation.RELATIVE_TO_SELF, 0.5f);
    
        scaleAnimation.setDuration(duration);
        scaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
    
        Display display = getWindowManager().getDefaultDisplay();
    
        int H;
    
        if(Build.VERSION.SDK_INT >= 13){
            Point size = new Point();
            display.getSize(size);
            H = size.y;
        }
        else{
            H = display.getHeight();
        }
    
        float h = ((float)finishView.getHeight());
    
        float verticalDisplacement = (-(H/2)+(3*h/4));
    
        TranslateAnimation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, 0,
                                                                       Animation.ABSOLUTE, 0,
                                                                       Animation.ABSOLUTE, 0,
                                                                       Animation.ABSOLUTE, verticalDisplacement);
    
        translateAnimation.setDuration(duration);
        translateAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
    
        AnimationSet animationSet = new AnimationSet(false);
        animationSet.addAnimation(scaleAnimation);
        animationSet.addAnimation(translateAnimation);
        animationSet.setFillAfter(false);
    
        startView.startAnimation(animationSet);
    }
    

    The key factor here is the value of toYDelta in the TranslateAnimation parameter:

    toYDelta = (-(H/2)+(3*h/4));
    

    Understanding why this works is the main thing. The rest is mostly simple.