Android过渡动画学习

概述

上篇笔记中对于Transition的框架和常用的API使用进行了分析,Transition最常用的是在界面过渡方面,本文继续学习Transition在界面过渡上的使用。在界面过渡上,Transition分为不带共享元素的Content Transition和带共享元素的ShareElement Transition。

Content Transition

先看下content transition的一个例子,在Google Play Games上的应用:

在经过学习后我们也可以设计出类似的效果,首先需要了解在界面过渡中涉及到的一些重要方法,从ActivtyA调用startActivity方法唤起ActivityB,到ActivityB按返回键返回ActivityA涉及到与Transition有关的方法

  • ActivityA.exitTransition()
  • ActivityB.enterTransition()

  • ActivityB.returnTransition()
  • ActivityA.reenterTransition()

因此,只要我们在对应的方法中设置了Transition就可以了。在默认没有设置对应Transition的情况下,Material-theme应用的exitTransition为null,enterTransition为Fade,如果reenterTransition和returnTransition未设定,则分别对应exitTransition和enterTransition。

使用

在style中添加android:windowContentTransitions 属性启用窗口内容转换(Material-theme应用默认为true),指定该Activity的Transition

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- enable window content transitions -->
    <item name="android:windowContentTransitions">true</item>
    <!-- specify enter and exit transitions -->
    <!-- options are: explode, slide, fade -->
    <item name="android:windowEnterTransition">@transition/change_image_transform</item>
    <item name="android:windowExitTransition">@transition/change_image_transform</item>
</style>

也可以在代码中指定

// inside your activity (if you did not enable transitions in your theme)
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
// set an enter transition
getWindow().setEnterTransition(new Explode());
// set an exit transition
getWindow().setExitTransition(new Explode());

然后启动Acticity

startActivity(intent,
              ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

例子

这里在代码中指定ActivityA的exitTransition:

private void setupTransition() {
        Slide slide = new Slide(Gravity.LEFT);
        slide.setDuration(1000);
        slide.setInterpolator(new FastOutSlowInInterpolator());
        getWindow().setExitTransition(slide);
    }

使用xml方式指定ActivityB的enterTransition:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <slide
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"
        android:slideEdge="bottom">
        <targets>
            <target android:targetId="@id/content_container"/>
        </targets>
    </slide>
    <slide
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"
        android:slideEdge="top">
        <targets>
            <target android:targetId="@id/image_container"/>
        </targets>
    </slide>
</transitionSet>

运行效果如下:

上图动画有两个问题:

1.ActivityA的exitTransition还没完全走完ActivityB的enterTransition就执行了,ActivityB的returnTransition还没完全走完ActivityA的reenterTransition就执行了;

2.状态栏和导航栏的动画不太协调;

问题1是因为默认情况下enter/return transition会比exit/reenter transition完全结束前稍微快一点运行,如果想让前者完全运行完后者再进来,可以在代码中调用Window

setWindowAllowEnterTransitionOverlap(false)
setWindowAllowReturnTransitionOverlap(false)

或者在xml中设置

<item name="android:windowAllowEnterTransitionOverlap">false</item> 
<item name="android:windowAllowReturnTransitionOverlap">false</item>

运行如下:

再看下问题2,默认情况下状态栏和标题栏也会参与动画(如果有导航栏也会,测试机默认木有导航栏),当我们把xxxoverlap属性设为false后就看得比较明显了,如果不想让它们参与动画通过excludeTarget()将其排除,在代码中:

private void setupTransition() {
    Slide slide = new Slide(Gravity.LEFT);
    slide.setDuration(1000);
    slide.setInterpolator(new FastOutSlowInInterpolator());
    slide.excludeTarget(android.R.id.statusBarBackground, true);
    slide.excludeTarget(android.R.id.navigationBarBackground, true);
    slide.excludeTarget(R.id.appbar,true);
    getWindow().setExitTransition(slide);
}

或者在xml中:

<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:slideEdge="left"
    android:interpolator="@android:interpolator/fast_out_slow_in"
    android:duration="1000">
    <targets>
        <!-- if using a custom Toolbar container, specify the ID of the AppBarLayout -->
        <target android:excludeId="@id/appbar" />
        <target android:excludeId="@android:id/statusBarBackground"/>
        <target android:excludeId="@android:id/navigationBarBackground"/>
    </targets>
</slide>

效果如下:

具体流程

ActivityA startActivity() 1.确定需要执行exit Transition的target View 2.Transition的captureStartValues()获取target View Visibility的值(此时为VISIBLE) 3.将target View Visibility的值设为INVISIBLE 4.Transition的captureEndValues()获取target View Visibility的值(此时为INVISIBLE) 5.Transition的createAnimator()根据前后Visibility的属性值变化创建动画

ActivityB Activity 开始 1.确定需要执行enter Transition的target View 2.Transition的captureStartValues()获取获取target View Visibility的,初始化为INVISIBLE 3.将target View Visibility的值设为VISIBLE 4.Transition的captureEndValues()获取target View Visibility的值(此时为VISIBLE) 5.Transition的createAnimator()根据前后Visibility的属性值变化创建动画

ShareElement Transition

shareElement Transition的例子

shareElement Transition指的是共享元素从activity/fragment到其他activity/fragment时的动画

有了上面的分析看名字应该也猜得出方法对应的功能了,如果没有设置exit/enter shared element transitions,默认为 @android:transition/move,上面的Content Transition是根据Visibility的变化创建动画,而shareElement Transition是根据大小,位置,和外观的变化创建动画,如chanageBounds、changeTransform、ChangeClipBounds、 ChangeImageTransform等,具体API使用和效果可以参考上篇。指定shareElement Transition可以通过代码形式:

getWindow().setSharedElementEnterTransition();
getWindow().setSharedElementExitTransition();
getWindow().setSharedElementReturnTransition();
getWindow().setSharedElementReenterTransition();

也可以通过xml形式:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- specify shared element transitions -->
    <item name="android:windowSharedElementEnterTransition">
      @transition/change_image_transform</item>
    <item name="android:windowSharedElementExitTransition">
      @transition/change_image_transform</item>
</style>

然后启动Acticity

Intent intent = new Intent(this, DetailsActivity.class);
// Pass data object in the bundle and populate details activity.
intent.putExtra(DetailsActivity.EXTRA_CONTACT, contact);
ActivityOptionsCompat options = ActivityOptionsCompat.
    makeSceneTransitionAnimation(this, (View)ivProfile, "profile");
startActivity(intent, options.toBundle());

在布局文件中对于要共享的View添加android:transitionName且保持一致,如果要共享的View有点多,可以通过Pair,Pair<View,String> 存储着共享View和View的名称,使用如下

Intent intent = new Intent(context, DetailsActivity.class);
intent.putExtra(DetailsActivity.EXTRA_CONTACT, contact);
Pair<View, String> p1 = Pair.create((View)ivProfile, "profile");
Pair<View, String> p2 = Pair.create(vPalette, "palette");
Pair<View, String> p3 = Pair.create((View)tvName, "text");
ActivityOptionsCompat options = ActivityOptionsCompat.
    makeSceneTransitionAnimation(this, p1, p2, p3);
startActivity(intent, options.toBundle());

例子

在ActivityB的theme中添加SharedElementEnterTransition

<item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform
</item>

change_image_transform.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"/>
    <changeImageTransform
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"/>
</transitionSet>

执行效果:

具体流程

从图上看,好像图片是从一个ActivityA"传递"到另一个ActivityB,实际上真正负责绘制都发生在ActivityB上:

1.ActivityA调用startActivity()后ActivityB处于透明状态

2.Transition收集ActivityA中共享View的初识状态,并传递给ActivityB

3.Transition收集ActivityB中共享View的最终状态

4.Transition根据状态改变创建动画

5.Transition隐藏ActivityA,随着ActivityB的共享View运动到指定位置,ActivityB的背景在ActivityA上淡入,并随着动画完成而完全可见。

我们可以通过修改Activity背景淡入淡出时间来验证,在ActivityB中加入

getWindow().setTransitionBackgroundFadeDuration(2000);

为了更直观,把ActivityA的exitTransition先注释掉,运行效果:

可以看到,ActivityB确实像盖在ActivityA上,这里用到了 ViewOverlay,原理简单来说就是在其他View上draw,共享View利用该技术可以实现画在其他View上。我们可以通过WindowsetSharedElementsUseOverlay(false)来关闭该功能,不过这样一来会使最终结果和你预想的不一致,默认该值为true。

延迟加载

上面分析Transition会获取共享视图前后的状态值来创建动画,如果我们的图片是网上下载的,那么很有可能图片的准确大小需要下载下来才能确定,Activity Transitions API提供了一对方法暂时推迟过渡,直到我们确切地知道共享元素已经被适当的渲染和放置。在onCreate中调用postponeEnterTransition()(API >= 21)或者supportPostponeEnterTransition()(API < 21)延迟过渡;当图片的状态确定后,调用startPostponedEnterTransition()(API >= 21)或supportStartPostponedEnterTransition()(API < 21)恢复过渡,常见处理:

// ... load remote image with Glide/Picasso here
supportPostponeEnterTransition();
ivBackdrop.getViewTreeObserver().addOnPreDrawListener(
    new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            ivBackdrop.getViewTreeObserver().removeOnPreDrawListener(this);
            supportStartPostponedEnterTransition();
            return true;
        }
    }
);

Thanks to

本文代码