Android 培训

缩放View

编写:XizhiXu - 原文:http://developer.android.com/training/animation/zoom.html

这节课示范怎样实现点击缩放动画,这对相册很有用,他能允许相片从缩略图转换成原图并填充屏幕提供动画。

下面展示了触摸缩放动画效果是什么样子,它将缩略图扩大并填充屏幕。

如果你想跳过看整个例子,下载 App 样例然后运行缩放的例子。查看下列文件中的代码实现:

  • src/TouchHighlightImageButton.java(简单的helper类,当image button被按下它显示蓝色高亮)
  • src/ZoomActivity.java
  • layout/activity_zoom.xml

创建View

为你想缩放的内容创建一大一小两个版本布局文件。下面的例子为可点击的缩略图新建了一个 ImageButton 和一个 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">

        <ImageButton
            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" />

    </LinearLayout>

    <!-- 这个初始化状态为隐藏的ImageView将会持有一个扩大/缩放版本的图片,并且浮于布局上层,
         没有动画施加在上面,并且占据整个屏幕。要实现“缩放”的动画,这个View是从上面的缩
         略图按钮的边界开始,扩大至最终的放大后的边界。
         -->

    <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>

设置缩放动画

一旦实现了布局,你需要设置触发缩放事件handler。下面的例子为ImageButton添加了一个View.OnClickListener,当用户点击按钮时它执行放大动画。

public class ZoomActivity extends FragmentActivity {
    // 持有一个当前animator的引用,
    // 以后以便于中途取消动画.
    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);
            }
        });

        //获取并缓存系统默认定义的“短”动画时长
        mShortAnimationDuration = getResources().getInteger(
                android.R.integer.config_shortAnimTime);
    }
    ...
}

缩放View

你现在需要适时应用放大动画了。通常来说,你需要按边界来从小号View放大到大号View。下面的方法告诉你如何实现缩放动画:

  1. 把高清图像设置到“放大版”隐藏的ImageView中。为表简单,下面的例子在 UI 线程中加载了一张大图。但是你需要在一个单独的线程中来加载以免阻塞 UI 线程,然后再回到 UI 线程中设置。理想状况下,图片不要大过屏幕。

  2. 计算ImageView开始和结束时的边界。

  3. 同步地动态改变四个位置和大小属性XYSCALE_XSCALE_Y),从起始点到结束点。这四个动画被加入到了AnimatorSet,所以你可以一起开始。

  4. 缩回则运行相同的动画,但是是用户点击屏幕放大时的逆向效果。你可以在ImageView中添加一个View.OnClickListener来实现它。当点击时,ImageView缩回到原来缩略图的大小,然后设置它的visibility为GONE来隐藏。

private void zoomImageFromThumb(final View thumbView, int imageResId) {
    //如果一个动画正在进行过程中,那么就要立即取消之前的动画并进行这一个。
    if (mCurrentAnimator != null) {
        mCurrentAnimator.cancel();
    }

    // 载入一个高分辨率的所谓 "已放大" 的图片.
    final ImageView expandedImageView = (ImageView) findViewById(
            R.id.expanded_image);
    expandedImageView.setImageResource(imageResId);

    // 为放大的图片计算开始动画和结束动画的矩形边界
    // 这个步骤牵扯到了大量的数学计算,YEAH!!坑爹的数学!!
    final Rect startBounds = new Rect();
    final Rect finalBounds = new Rect();
    final Point globalOffset = new Point();

    // 动画开始的边界是缩略图对全局可见的矩形,最终的边界是外部包裹的布局可见矩形。
    // 这里还设置了包裹视图的偏移量为原点的边界,因为这是原点为定位的动画属性(X, Y)。
    thumbView.getGlobalVisibleRect(startBounds);
    findViewById(R.id.container).getGlobalVisibleRect(finalBounds, globalOffset);
    startBounds.offset(-globalOffset.x, -globalOffset.y);
    finalBounds.offset(-globalOffset.x, -globalOffset.y);

    // 调整开始边界要和使用了“centerCrop”技术的最终边界保持相同的纵横比。
    // 这可以在动画过程中防止不希望出现的拉伸现象。还计算了开始大小的缩放系数
    // (结束大小的系数则一直为1.0)
    float startScale;
    if ((float) finalBounds.width() / finalBounds.height()
            > (float) startBounds.width() / startBounds.height()) {
        // 水平扩展开始边界
        startScale = (float) startBounds.height() / finalBounds.height();
        float startWidth = startScale * finalBounds.width();
        float deltaWidth = (startWidth - startBounds.width()) / 2;
        startBounds.left -= deltaWidth;
        startBounds.right += deltaWidth;
    } else {
        // 竖直扩展开始边界
        startScale = (float) startBounds.width() / finalBounds.width();
        float startHeight = startScale * finalBounds.height();
        float deltaHeight = (startHeight - startBounds.height()) / 2;
        startBounds.top -= deltaHeight;
        startBounds.bottom += deltaHeight;
    }

    // 隐藏缩略图并显示放大后的View。当动画开始,将在缩略图的位置定位放大的视图
    thumbView.setAlpha(0f);
    expandedImageView.setVisibility(View.VISIBLE);

    // 设置锚点,以放大后的View左上角坐标为准来准备 SCALE_X 和 SCALE_Y 变换
    // (默认为View的中心)
    expandedImageView.setPivotX(0f);
    expandedImageView.setPivotY(0f);

    // 构建并并行化运行4个平移动画和缩放属性(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;

    // 点击放大后的图片,应该是缩放回原来的边界并显示缩略图
    // 而不是显示扩大的图
    final float startScaleFinal = startScale;
    expandedImageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mCurrentAnimator != null) {
                mCurrentAnimator.cancel();
            }

            // 开始并行动画这四个位置/大小属性,直到归至原始值。
            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;
        }
    });
}