AnimationsDemo中的ZoomActivity代码分析

AnimationsDemo是android官网的一个动画使用示例。

ZoomActivity是demo中的图像缩放动画,因为这种效果比较常见,所以研究了一下代码。

下面是效果图:

技术分享

毫无疑问这是一个组合动画,translation和scale动画.实现这种动画的关键是如何确定动画的坐标和缩放比例

除了一些简单的数学计算外,该demo还利用了ImageView的fitCenter特性.稍后我们就可以看到.

在开始分析代码之前,先说一下程序的原理:

    1,点击缩略图的时候同时将缩略图隐藏。

    2,载入相应的大图,将大图缩小成缩略图的大小,并设置为Visible

    3,大图缩小后移动到原缩略图的位置,并把它覆盖

    4,被缩小的大图在该位置重新放大

 

为了更清楚的表达这个过程,我将程序改动一下再运行:

技术分享

浅绿色部分就是整个ImageView的大小。明白这一点很重要。

 

原理明白了就可以开始分析代码,先来的是程序的布局:

<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="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            style="?android:textAppearanceSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/message_zoom_touch_expand"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/thumb_button_1"
                android:layout_width="100dp"
                android:layout_height="75dp"
                android:layout_marginRight="1dp"
                android:src="@drawable/thumb1"
                android:scaleType="centerCrop"
                android:contentDescription="@string/description_image_1"/>

            <ImageView
                android:id="@+id/thumb_button_2"
                android:layout_width="100dp"
                android:layout_height="75dp"
                android:src="@drawable/thumb2"
                android:scaleType="centerCrop"
                android:contentDescription="@string/description_image_2"/>

        </LinearLayout>

    </LinearLayout>

    <ImageView
        android:id="@+id/expanded_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="invisible"
        android:contentDescription="@string/description_zoom_touch_close"/>

</FrameLayout>

布局本身并没有什么值得讨论的地方,唯一需要注意的是布局中的三个ImageView对象。

两个用于放置缩略图,亦即是上图中的两个小图,最下面的ImageView就是我们主要操作的对象。

这样做的好处是可以节省程序的使用内存,防止OOM的发生。

 

大概了解一下布局后我们就可以来分析程序的逻辑,程序中所有的动画逻辑都在下面的函数中完成

private void zoomImageFromThumb(final View thumbView, int imageResId)

 

下面是函数的其中一段代码:

//用于计算translation动画开始的坐标
        final Rect startBounds = new Rect();
        final Rect finalBounds = new Rect();
        final Point globalOffset = new Point();

        //获取thumbView在屏幕中的偏移量
        thumbView.getGlobalVisibleRect(startBounds);
        //获取container在屏幕中的偏移量并将偏移量记录到globalOffset中
        findViewById(R.id.container).getGlobalVisibleRect(finalBounds, globalOffset);

        //将屏幕坐标减去ActionBar+StatusBar的高度
        startBounds.offset(-globalOffset.x, -globalOffset.y);
        finalBounds.offset(-globalOffset.x, -globalOffset.y);

 

对getGlobalVisibleRect函数不明白的可以参考下面的文章

GetGlobalVisibleRect和getLocalVisibleRect

上面这段代码的主要作用就是获取缩略图的坐标,因为动画就是从这个坐标开始进行。

startBounds和finalBounds调用offset方法的作用是将坐标转换为以Activity左上角为原点的坐标

 

坐标计算好后就开始计算缩放比率,下面是函数的另一段代码:

float startScale;
        if ((float) finalBounds.width() / finalBounds.height()
                > (float) startBounds.width() / startBounds.height())
        {
            // Extend start bounds horizontally
            startScale = (float) startBounds.height() / finalBounds.height();
            System.out.println("startScale1:"+startScale);
            float startWidth = startScale * finalBounds.width();
            float deltaWidth = (startWidth - startBounds.width()) / 2;
            System.out.println("startWidth:"+startWidth);
            System.out.println("deltaWidth:"+deltaWidth);
            startBounds.left -= deltaWidth;
            startBounds.right += deltaWidth;

        } else
        {
            //计算缩放量比例
            startScale = (float) startBounds.width() / finalBounds.width();
            //计算expanded_image缩小后的大小
            float startHeight = startScale * finalBounds.height();
            //计算expanded_image上下空间的偏移距离
            float deltaHeight = (startHeight - startBounds.height()) / 2;
            //开始移动动画前的位置
            startBounds.top -= deltaHeight;
            startBounds.bottom += deltaHeight;

        }

根据下面的图在来分析代码

技术分享

我们都知道绿色部分才是ImageView的覆盖位置,所以位移开始的地方是绿色部分的左上角

startBounds和finalBounds的坐标实际上并不包含上下两个绿色矩形,因此我们要纠正之前获取的坐标

下面几行代码的作用就是用于纠正坐标和计算缩放比例

startScale = (float) startBounds.width() / finalBounds.width();
            //计算expanded_image缩小后的大小
            float startHeight = startScale * finalBounds.height();
            //计算expanded_image上下空间的偏移距离
            float deltaHeight = (startHeight - startBounds.height()) / 2;
            //开始移动动画前的位置
            startBounds.top -= deltaHeight;
            startBounds.bottom += deltaHeight;

startHeight是缩放后整个绿色部分的高度,deltaHeight就是上下两个矩形的各自高度,它们的值相等。

startBounds.top减去deltaHeight的高度就可以将startBounds的坐标向上移动。因为原点在左上角,要向上移动就要用减号。

startBounds.bottom的原理相同。

 

准备工作都做好后,动画开始播放:

        expandedImageView.setVisibility(View.VISIBLE);
        expandedImageView.setPivotX(0f);
        expandedImageView.setPivotY(0f);

        AnimatorSet set = new AnimatorSet();
        set
                .play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left,
                        finalBounds.left))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top,
                        finalBounds.top))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
        set.setDuration(mShortAnimationDuration);
        set.setInterpolator(new DecelerateInterpolator());
        set.start();

正如我们前面说的需要将大图设置为显示:expandedImageView.setVisibility(View.VISIBLE);

下面的代码将中心点移动到expandedImageView的左上角

expandedImageView.setPivotX(0f); 

expandedImageView.setPivotY(0f);

剩下的代码基本上就是如何使用Property Animation,不熟悉的可参考使用属性动画 — Property Animation

 

Demo的完整代码:

package com.example.android.animationsdemo;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Intent;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;


public class ZoomActivity extends FragmentActivity
{
    private Animator mCurrentAnimator;

    private int mShortAnimationDuration;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_zoom);

        final View thumb1View = findViewById(R.id.thumb_button_1);
        thumb1View.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View view)
            {
                zoomImageFromThumb(thumb1View, R.drawable.image1);
            }
        });

        final View thumb2View = findViewById(R.id.thumb_button_2);
        thumb2View.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View view)
            {
                zoomImageFromThumb(thumb2View, R.drawable.image2);
            }
        });

        // Retrieve and cache the system‘s default "short" animation time.
        mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        switch (item.getItemId())
        {
            case android.R.id.home:
                // Navigate "up" the demo structure to the launchpad activity.
                // See http://developer.android.com/design/patterns/navigation.html for more.
                NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class));
                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void zoomImageFromThumb(final View thumbView, int imageResId)
    {
        // If there‘s an animation in progress, cancel it immediately and proceed with this one.
        if (mCurrentAnimator != null)
        {
            mCurrentAnimator.cancel();
        }

        // Load the high-resolution "zoomed-in" image.
        final ImageView expandedImageView = (ImageView) findViewById(R.id.expanded_image);
        expandedImageView.setImageResource(imageResId);


        //用于计算translation动画开始的坐标
        final Rect startBounds = new Rect();
        final Rect finalBounds = new Rect();
        final Point globalOffset = new Point();

        //获取thumbView在屏幕中的偏移量
        thumbView.getGlobalVisibleRect(startBounds);
        //获取container在屏幕中的偏移量并将偏移量记录到globalOffset中
        findViewById(R.id.container).getGlobalVisibleRect(finalBounds, globalOffset);

        //将屏幕坐标减去ActionBar+StatusBar的高度
        startBounds.offset(-globalOffset.x, -globalOffset.y);
        finalBounds.offset(-globalOffset.x, -globalOffset.y);


        float startScale;
        if ((float) finalBounds.width() / finalBounds.height()
                > (float) startBounds.width() / startBounds.height())
        {
            // Extend start bounds horizontally
            startScale = (float) startBounds.height() / finalBounds.height();
            System.out.println("startScale1:"+startScale);
            float startWidth = startScale * finalBounds.width();
            float deltaWidth = (startWidth - startBounds.width()) / 2;
            System.out.println("startWidth:"+startWidth);
            System.out.println("deltaWidth:"+deltaWidth);
            startBounds.left -= deltaWidth;
            startBounds.right += deltaWidth;

        } else
        {
            //计算缩放量比例
            startScale = (float) startBounds.width() / finalBounds.width();
            //计算expanded_image缩小后的大小
            float startHeight = startScale * finalBounds.height();
            //计算expanded_image上下空间的偏移距离
            float deltaHeight = (startHeight - startBounds.height()) / 2;
            //开始移动动画前的位置
            startBounds.top -= deltaHeight;
            startBounds.bottom += deltaHeight;

        }
        // Hide the thumbnail and show the zoomed-in view. When the animation begins,
        // it will position the zoomed-in view in the place of the thumbnail.
        thumbView.setAlpha(0f);
        expandedImageView.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).
        expandedImageView.setPivotX(0f);
        expandedImageView.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(expandedImageView, View.X, startBounds.left,
                        finalBounds.left))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top,
                        finalBounds.top))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
        set.setDuration(mShortAnimationDuration);
        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-in image, it should zoom back down to the original bounds
        // and show the thumbnail instead of the expanded image.
        final float startScaleFinal = startScale;
        expandedImageView.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(expandedImageView, View.X, startBounds.left))
                    .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
                    .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
                    .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
                set.setDuration(mShortAnimationDuration);
                set.setInterpolator(new DecelerateInterpolator());
                set.addListener(new AnimatorListenerAdapter()
                {
                    @Override
                    public void onAnimationEnd(Animator animation)
                    {
                        thumbView.setAlpha(1f);
                        expandedImageView.setVisibility(View.GONE);
                        mCurrentAnimator = null;
                    }

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

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。