[Android] Transition

1,301 阅读3分钟

What is Transition?

从一个页面切换到另一个页面,布局发生变化的时候,添加转场动画不管是触觉、视觉还是前后页面逻辑关系上,都会改善用户体验。

transition framework是AndroidX Transition Library中提供的一套API,兼容到14. 通过它可以简单地实现两个视图元素之间地动画切换。提供了几个内建动画类型,同时也允许用户自定义。

Transition 包含了完成场景切换动画所需的信息,它本身是一个抽象类,其子类可能包含多个子转场动画信息,TransitionSet或者自定义动画。任何一个转场动画包括两个主要部分:1. 捕获属性值,2. 属性值发生变化时应用的动画效果。

Note: 由于在屏幕上展示渲染方式, TransitionSurfaceView/TextureView同时使用效果可能不佳。比如SurfaceView,它的更新时在一个non-UI thread中进行,所以transitions带来的view更新(move,resize, UI thread)与渲染SurfaceView的不同步;TextureView与Transitions大体上兼容性更好,但是一些特殊的动画,比如Fade,由于它依赖于ViewOverlay方法,但是在TextureView上并不生效。

How to Apply Transition?

Mainly: Start an activity using an animation

Define Transition style

  1. 几种内建transition类型:

例如:fade:

<fade xmlns:android="http://schemas.android.com/apk/res/android"/>

expode:

<explode xmlns:android="http://schemas.android.com/apk/res/android"/>

move:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
  <changeBounds/>
  <changeTransform/>
  <changeClipBounds/>
  <changeImageTransform/>
</transitionSet>
  1. 自定义style

新建transition动画,默认在main/res/transition目录下

Activity的theme中指定

<style name="BaseAppTheme" parent="android:Theme.Material">
  <!-- enable window content transitions -->
  <item name="android:windowActivityTransitions">true</item>

  <!-- specify enter and exit transitions -->
  <item name="android:windowEnterTransition">@transition/explode</item>
  <item name="android:windowExitTransition">@transition/explode</item>

  <!-- specify shared element transitions -->
  <item name="android:windowSharedElementEnterTransition">
    @transition/change_image_transform</item>
  <item name="android:windowSharedElementExitTransition">
    @transition/change_image_transform</item>
</style>

Start an activity using transitions

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

Start an activity with shared element

// get the element that receives the click event
val imgContainerView = findViewById<View>(R.id.img_container)

// get the common element for the transition in this activity
val androidRobotView = findViewById<View>(R.id.image_small)

// define a click listener
imgContainerView.setOnClickListener( {
    val intent = Intent(this, Activity2::class.java)
    // create the transition animation - the images in the layouts
    // of both activities are defined with android:transitionName="robot"
    val options = ActivityOptions
            .makeSceneTransitionAnimation(this, androidRobotView, "robot")
    // start the new activity
    startActivity(intent, options.toBundle())
})

multiple shared elements

// Rename the Pair class from the Android framework to avoid a name clash
import android.util.Pair as UtilPair
...
val options = ActivityOptions.makeSceneTransitionAnimation(this,
        UtilPair.create(view1, "agreedName1"),
        UtilPair.create(view2, "agreedName2"))

常见问题

1, status、navigation bar 会闪

Approach #1: Exclude the status bar and navigation bar from the window's default exit/enter fade transition

The reason why the navigation/status bar are fading in and out during the transition is because by default all non-shared views (including the navigation/status bar backgrounds) will fade out/in in your calling/called Activitys respectively once the transition begins. You can, however, easily get around this by excluding the navigation/status bar backgrounds from the window's default exit/enter Fade transition. Simply add the following code to your Activitys' onCreate() methods:

Transition fade = new Fade();
fade.excludeTarget(android.R.id.statusBarBackground, true);
fade.excludeTarget(android.R.id.navigationBarBackground, true);
getWindow().setExitTransition(fade);
getWindow().setEnterTransition(fade);

This transition could also be declared in the activity's theme using XML (i.e. in your own res/transition/window_fade.xml file):

<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android">
    <targets>
        <target android:excludeId="@android:id/statusBarBackground"/>
        <target android:excludeId="@android:id/navigationBarBackground"/>
    </targets>
</fade>

Approach #2: Add the status bar and navigation bar as shared elements - my choice

In calling Activity, use the following code to start the Activity:

View statusBar = findViewById(android.R.id.statusBarBackground);
View navigationBar = findViewById(android.R.id.navigationBarBackground);

List<Pair<View, String>> pairs = new ArrayList<>();
if (statusBar != null) {
  pairs.add(Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME));
}
if (navigationBar != null) {
  pairs.add(Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME));
}
pairs.add(Pair.create(mSharedElement, mSharedElement.getTransitionName()));

Bundle options = ActivityOptions.makeSceneTransitionAnimation(activity, 
        pairs.toArray(new Pair[pairs.size()])).toBundle();
startActivity(new Intent(context, NextActivity.class), options);

Add the following code in called Activity's onCreate() method in order to prevent the status bar and navigation bar from "blinking" during the transition:

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

    // Postpone the transition until the window's decor view has
    // finished its layout.
    postponeEnterTransition();

    final View decor = getWindow().getDecorView();
    decor.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            decor.getViewTreeObserver().removeOnPreDrawListener(this);
            startPostponedEnterTransition();
            return true;
        }
    });
}

Adding the status bar and navigation bar backgrounds as shared elements will force them to be drawn on top of the window's default exit/enter fade transition, meaning that they will not fade during the transition.

transition给activity生命周期带来的影响

常见情况下是一样的,例如:Activity A -> B -> finish B -> finish A, 生命周期变化与加了transition动画是一样的,为:

onCreate: A -> onResume: A
onPause: A -> onCreate: B -> onResume: B
onPause: B -> onResume: A -> onDestroy: B
onPause: A -> onDestroy: A

BUT,对于中途finish某个Activity,例如:Activity A -> B -> C同时finish B, 正常的生命周期变化是:

onCreate: A -> onResume: A
onPause: A -> onCreate: B -> onResume: B
onPause: B -> onCreate: C -> onResume: C -> onDestroy: B

如果,Activity A -> transit B -> transit C同时finish B, 生命周期变化是:

onCreate: A -> onResume: A
onPause: A -> onCreate: B -> onResume: B
onPause: B -> onCreate: C -> onResume: C -> onDestroy: B -> onResume: A -> onPause: A 
// or 
onPause: B -> onCreate: C -> onResume: C -> onResume: A -> onPause: A -> onDestroy: B

除了对生命周期带来异常外,转场动画本身也有出现闪烁,why? how to fix?

why:

对于transition启动的Activity,就像启动那样makeSceneTransitionAnimation,它在结束的时候虽然可以指定要不要transition但是默认的会有一个scale的动画,这个是没法重写的,(有人说overridePendingTransition可以抹去,但是试过不行)。

how:

  • start C without transition
  • finish B with delay

这两种方式都能使生命周期恢复正常onPause: B -> onCreate: C -> onResume: C -> onDestroy: B,但是第一种会导致转场动画失效,从体验上还是第二种更优雅一些。