为View的切换添加过渡动画

  动画效果不仅可以使得应用更加吸引人,更可以突出变化的内容,使得用户能够更好地理解应用的操作和运作方式。Android提供了Transitions Framework来为View层级之间的切换添加过渡效果,最低需要API level 19。下面通过一个例子说明如何 添加过渡动画。

  例子以在Android上实现Master/Detail Flow为基础,最终实现效果如下:

1. 添加Transition

1.1. 添加transitionSet

  transitionSet包含了一组动画效果。新建/res/transition-v21/文件夹,不在低于Android L的版本上显示动画。

  在/res/transition-v21下新建detail_window_enter_transition.xml,设置用于显示进入MovieDetailActivity(Detail页面)的过渡效果:

<transitionSet
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together"
    android:duration="500">
    <fade>
        <targets>
            <target android:excludeId="@android:id/statusBarBackground"/>
            <target android:excludeId="@android:id/navigationBarBackground"/>
        </targets>
    </fade>
    <slide android:slideEdge="top">
        <targets>
            <target android:targetId="@id/app_bar" />
        </targets>
    </slide>
</transitionSet>

这里android:duration 为动画持续时间,单位为毫秒。<fade> 表示淡入效果,<targets> 用于指示动画作用的目标,这里用android:excludeId 排除了状态栏和底部虚拟按钮。<slide android:slideEdge=”top”> 表示滑入动画,滑入方向为上方,作用于app_bar 。

【完整代码】

  类似地,新建detail_window_return_transition.xml,设置退出MovieDetailActivity(Detail页面)的过渡效果:

<transitionSet
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together"
    android:duration="500">
    <fade>
        <targets>
            <target android:excludeId="@android:id/statusBarBackground" />
            <target android:excludeId="@android:id/navigationBarBackground" />
        </targets>
    </fade>
</transitionSet>

【完整代码】

1.2. 设置Theme

  为了启用过渡效果,需要在Activity的Theme中设置android:windowContentTransitions 为true ,并指明所使用的transitionSet。这里的例子使用了Master/Detail Flow,为MovieGridActivity(Master页面)设置主题AppTheme.Main ,为MovieDetailActivity(Detail页面)设置主题AppTheme.Detail 。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <application
        ...
        android:theme="@style/AppTheme">
        <activity
            android:name=".ui.activity.MovieGridActivity"
            ...
            android:theme="@style/AppTheme.Main">
            ...
        </activity>
        <activity
            android:name=".ui.activity.MovieDetailActivity"
            ...
            android:theme="@style/AppTheme.Detail">
            ...
        </activity>
    </application>
</manifest>

【完整代码】

  在/res/values-v21/styles.xml中为过渡前后的两个Activity打开android:windowContentTransitions ,并为MovieDetailActivity(Detail页面)的主题AppTheme.Detail 指定transitionSet:

<resources>
    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>

    <style name="AppTheme.Main" parent="@style/AppTheme.NoActionBar">
        <item name="android:windowContentTransitions">true</item>
    </style>

    <style name="AppTheme.Detail" parent="@style/AppTheme.NoActionBar">
        <item name="android:windowContentTransitions">true</item>
        <item name="android:windowEnterTransition">@transition/detail_window_enter_transition</item>
        <item name="android:windowReturnTransition">@transition/detail_window_return_transition</item>
    </style>
</resources>

【完整代码】

  在/res/values/styles.xml中依然需要定义AppTheme.Main 和AppTheme.Detail ,在Android L之前版本上不显示过渡动画:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

<style name="AppTheme.NoActionBar">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
</style>

<style name="AppTheme.Main" parent="@style/AppTheme.NoActionBar"/>
<style name="AppTheme.Detail" parent="@style/AppTheme.NoActionBar"/>

【完整代码】

1.3. 启动Activity

  最后,使用ActivityOptionsCompat通过ActivityCompat来启动Activity,过渡动画就会在进入Detail Activity时播放。

Intent intent = new Intent(this, MovieDetailActivity.class)
                    .putExtra(MovieDetailActivity.MOVIE_INFO, movie);
ActivityOptionsCompat activityOptions = ActivityOptionsCompat
                    .makeSceneTransitionAnimation(this);
ActivityCompat.startActivity(this, intent, activityOptions.toBundle());

2. 添加Shared Element Transition

  如果切换前后的两个界面具有相同的元素,那么可以使用Shared Element Transition为相同元素添加过渡效果。在这个例子中,MovieGridActivity(Master页面)和MovieDetailActivity(Detail页面)都具有电影海报这个元素,下面为电影海报添加Shared Element Transition。

2.1. 指定transitionName

  首先参考1.2.中的步骤,在Activity的Theme中设置android:windowContentTransitions 为true 。

  然后需要为Master Activity和Detail Activity中用于显示电影海报的ImageView添加transitionName,将这两个ImageView关联起来。首先定义transitionName的字符串:

<string name="detail_poster_transition_name" translatable="false">TN_DetailPoster</string>

  MovieGridActivity(Master页面)的电影海报位于/res/layout/item_movie.xml:

<LinearLayout
    ...>
    <ImageView
        android:id="@+id/movie_poster"
        ...
        android:transitionName="@string/detail_poster_transition_name"
        ... />
...
</LinearLayout>

【完整代码】

  MovieDetailActivity(Detail页面)的电影海报位于/res/layout/fragment_movie_detail.xml:

<LinearLayout
    ... >
    <LinearLayout
        ... >

        <ImageView
            android:id="@+id/detail_poster"
            ...
            android:transitionName="@string/detail_poster_transition_name"
            ... />
        ...
    </LinearLayout>
    ...
</LinearLayout>

【完整代码】

2.2. 启动进入和返回动画

  依旧用ActivityOptionsCompat通过ActivityCompat来启动Activity,这时需要在ActivityOptionsCompat中指明过度前后所共享的View(显示电影海报的ImageView),以及共享元素的名称(即之前设置的transitionName)。

Intent intent = new Intent(this, MovieDetailActivity.class)
        .putExtra(MovieDetailActivity.MOVIE_INFO, movie);
ActivityOptionsCompat activityOptions = ActivityOptionsCompat
        .makeSceneTransitionAnimation(this, new Pair<View, String>(
                        vh.mPoster,
                        getString(R.string.detail_poster_transition_name)));
ActivityCompat.startActivity(this, intent, activityOptions.toBundle());

【完整代码】

  然后还需要设置从MovieDetailActivity(Detail页面)返回MovieGridActivity(Master页面)的动画,在MovieDetailActivity中设置按下返回按钮时,调用supportFinishAfterTransition() :

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    if (id == android.R.id.home) {
        supportFinishAfterTransition();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

【完整代码】

3. 延迟过渡动画

  有时候在进行切换时,切换的目标元素还未载入完成,由此到会导致动画的不连贯。所以需要通过supportPostponeEnterTransition() 在MovieDetailActivity刚创建时,延迟动画效果,此时就算在MovieGridActivity(Master页面)的电影列表中点击了电影,画面会暂停,不会触发过渡动画:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_movie_detail);
    ...
    if (savedInstanceState == null) {
        ...
        supportPostponeEnterTransition();
        ...
    }
}

  在这个例子中,MovieDetailFragment中的数据由MovieGridActivity传递,耗时的操作在于MovieDetailActivity中下载背景图片。应当在图片下载并显示完成后,无论成功与否,都启动动画效果,防止卡死:

private void updateTitle() {
    ...
    ImageView backdrop = (ImageView) findViewById(R.id.detail_backdrop);
    String url = ImageUtility.getBackdropImageUrl(mMovie.getBackdropPath());
    Picasso.with(this).load(url).into(backdrop, new com.squareup.picasso.Callback() {
        @Override
        public void onError() {
            supportStartPostponedEnterTransition();
        }

        @Override
        public void onSuccess() {
            supportStartPostponedEnterTransition();
        }
    });
}

【完整代码】

4. 完整代码

  完整代码可以参考这里