为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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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"/>
<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"/>
<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());
Intent intent = new Intent(this, MovieDetailActivity.class) .putExtra(MovieDetailActivity.MOVIE_INFO, movie); ActivityOptionsCompat activityOptions = ActivityOptionsCompat .makeSceneTransitionAnimation(this); ActivityCompat.startActivity(this, intent, activityOptions.toBundle());
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>
<string name="detail_poster_transition_name" translatable="false">TN_DetailPoster</string>
<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>
<LinearLayout ...> <ImageView android:id="@+id/movie_poster" ... android:transitionName="@string/detail_poster_transition_name" ... /> ... </LinearLayout>
<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>
<LinearLayout ... > <LinearLayout ... > <ImageView android:id="@+id/detail_poster" ... android:transitionName="@string/detail_poster_transition_name" ... /> ... </LinearLayout> ... </LinearLayout>
<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());
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());
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);
}
@Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { supportFinishAfterTransition(); return true; } return super.onOptionsItemSelected(item); }
@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();
...
}
}
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_movie_detail); ... if (savedInstanceState == null) { ... supportPostponeEnterTransition(); ... } }
@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();
}
});
}
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(); } }); }
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. 完整代码

  完整代码可以参考这里