Android共享元素动画原理解析

Andriod 5.0及之后开始支持Activity之间的共享元素动画,共享元素即启动Activity的时候,Activity A中的控件能够通过动画的方式流畅的过渡到Activity B中对应的控件。
之前在项目中用到了共享元素动画,遇到了一些坑,碍于不了解底层原理&时间紧迫,实现的效果并不完美。最近得空带着一些疑惑阅读了一波源码,本文将带着这些问题来一步步解析源码,下图为实现效果。


PS:本文源码基于android-28

使用共享元素动画

使用共享元素动画很简单,只需要三步

  1. 通过给两个Activity设定Window.FEATURE_CONTENT_TRANSITIONS来启用transition api
  2. 给两个Activity对应的控件加上transitionName属性,且对应控件的transitionName应该保持一致
  3. 使用public void startActivity(Intent intent, @Nullable Bundle options)方法启动Activity
1
2
3
4
startActivity(Intent(this, TargetActivity::class.java), createTransitionBundle())
private fun createTransitionBundle(): Bundle? {
return ActivityOptionsCompat.makeSceneTransitionAnimation(this, Pair(avatar, "avatar"), Pair(bg, "bg"), Pair(user_name, "user_name")).toBundle()
}

ActivityOptionsCompat.makeSceneTransitionAnimation方法接受类型为Pair<View,String>的可变长数组,对应参与共享元素动画的View和transitionName,该方法可以支持多个共享元素。

通过以上方式就已经可以简单的实现共享元素动画,但是如果被启动的Activity中对应的View因为某些原因(等待网络请求等)不能立即展示,而是需要等待一段时间才能显示,那么在View没有显示的时候做共享元素动画显然是不合理的,所以Android提供了两个方法让我们可以延迟动画的开始时机

1
2
3
4
// 延迟enter动画
supportPostponeEnterTransition()
// 开始执行enter动画
supportStartPostponedEnterTransition()

我们可以在被启动的Activity的onCreate方法中调用supportPostponeEnterTransition()来延迟动画的执行,并在我们认为时机到了的时候(View被正常绘制后)调用supportStartPostponedEnterTransition()来开始执行共享元素动画。

共享元素动画大致流程

本节大致介绍一下从startActivity到动画执行完毕的过程中发生了什么事情,以便对共享元素动画有一个大致的认识,这样更加有利于理解后续的源码解析。
首先引入五个类,这五个类承担共享元素动画的大部分逻辑。

  • ResultReceiver:ResultReceiver是一个用来接收其他进程回调结果的通用接口。要使用它,需要创建一个子类并且实现onReceiveResult(int, android.os.Bundle)方法,在其他线程(进程)中可以通过send(int, android.os.Bundle)方法发送数据,底层实现是对Binder的简单封装
  • ActivityTransitionCoordinator:继承自ResultReceiver,是ExitTransitionCoordinator 和 EnterTransitionCoordinator的基类,负责管理Activity的动画和Activity之间的通信
  • EnterTransitionCoordinator:继承自ActivityTransitionCoordinator,负责启动Activity时的enter动画
  • ExitTransitionCoordinator:继承自ActivityTransitionCoordinator,负责启动Activity时的exit动画
  • ActivityTransitionState:与Activity交互,作为Activity与ActivityTransitionCoordinator间的沟通桥梁

一次典型的startActivity动画流程如下所示:

  1. ExitTransitionCoordinator在ActivityOptions#makeSceneTransitionAnimation方法中被创建,并将它传入options中返回
  2. Activity#startActivity最终调用到cancelInputsAndStartExitTransition()触发ExitTransitionCoordinator#startExit(),隐藏其余的View同时将SharedViews移动到顶层
  3. Activity B启动,通过ActivityTransitionState#enterReady来创建EnterTransitionCoordiantor,同时调用它的startEnter()做一些准备操作
    • MSG_SET_REMOTE_RECEIVER被发送给ExitTransitionCoordinator来设置相互的引用
    • 将Window设置为透明
    • Window的background设置为alpha=0
    • 将不参与SharedTransition的View和SharedViews设置为alpha=0
    • 将SharedViews移动到顶层
  4. ExitTransitionCoordinator的exit动画结束后发送MSG_TAKE_SHARED_ELEMENTS给EnterTransitionCoordinator,EnterTransitionCoordinator开始动画
    • SharedViews设置为alpha=1
    • SharedView的位置和大小被设置为启动Activity对应Views的原始状态
    • 开始共享元素动画
    • 发送MSG_HIDE_SHARED_ELEMENTS给ExitTransitionCoordinator通知启动Activity隐藏对应的SharedViews
  5. ExitTransitionCoordinator同时发送MSG_EXIT_TRANSITION_COMPLETE给EnterTransitionCoordinator,通知它可以开始EnterTransition动画(不是SharedEnterTransitino)
  6. 动画结束后Activity A回调onStop()触发被隐藏的Views恢复显示

Activity A的Shared Views是怎么传递给Activity B的

其实传递View这种说法并不准确,准确来说是将View的相关参数传递给Activity B,使得Activity B可以根据这些参数来构造出和Activity A上一样的View。
首先ExitTransitionCoordinator会在构造方法中调用父类的viewsReady(mapSharedElements(accepted, mapped))方法来收集当前Activity参与共享元素动画的所有View,并赋值给mSharedElements和mSharedElementNames,分别表示共享元素View和对应Name的集合,同时获取当前Activity中所有可见性为Visible的View并赋值给mTransitionViews

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ExitTransitionCoordinator.java
protected ArrayMap<String, View> mapSharedElements(ArrayList<String> accepted,
ArrayList<View> localViews) {
...
if (decorView != null) {
// 遍历child获得所有transitionName不为空且Visible的View
decorView.findNamedViews(sharedElements);
}
...
return sharedElements;
}

protected void viewsReady(ArrayMap<String, View> sharedElements) {
sharedElements.retainAll(mAllSharedElementNames);
if (mListener != null) {
mListener.onMapSharedElements(mAllSharedElementNames, sharedElements);
}
// 将mSharedElements和mSharedElementNames赋值
setSharedElements(sharedElements);
...
if (decorView != null) {
// 获取所有Visibile的View到mTransitioningViews中
decorView.captureTransitioningViews(mTransitioningViews);
}
mTransitioningViews.removeAll(mSharedElements);
...
}

随后ExitTransitionCoordinator会在ExitTransition执行完毕后会触发captureSharedElementState()来构造共享元素的相关参数,返回一个Bundle赋值给mSharedElementBundle,然后调用notifyComplete()将mSharedElementBundle发送给Activity B的EnterTransitionCoordinator通知可以开始动画

1
2
3
4
5
6
protected void notifyComplete() {
...
// resultReceiver就是Activity B的EnterTransitionCoordinator
resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, sharedElementBundle);
...
}

我们着重查看captureSharedElementState()方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ExitTransitionCoordinator.java
protected Bundle captureSharedElementState() {
Bundle bundle = new Bundle();
RectF tempBounds = new RectF();
Matrix tempMatrix = new Matrix();
for (int i = 0; i < mSharedElements.size(); i++) {
View sharedElement = mSharedElements.get(i);
String name = mSharedElementNames.get(i);
captureSharedElementState(sharedElement, name, bundle, tempMatrix, tempBounds);
}
return bundle;
}

protected void captureSharedElementState(View view, String name, Bundle transitionArgs,
Matrix tempMatrix, RectF tempBounds) {
Bundle sharedElementBundle = new Bundle();
tempMatrix.reset();
view.transformMatrixToGlobal(tempMatrix);
tempBounds.set(0, 0, view.getWidth(), view.getHeight());
tempMatrix.mapRect(tempBounds);

sharedElementBundle.putFloat(KEY_SCREEN_LEFT, tempBounds.left);
sharedElementBundle.putFloat(KEY_SCREEN_RIGHT, tempBounds.right);
sharedElementBundle.putFloat(KEY_SCREEN_TOP, tempBounds.top);
sharedElementBundle.putFloat(KEY_SCREEN_BOTTOM, tempBounds.bottom);
sharedElementBundle.putFloat(KEY_TRANSLATION_Z, view.getTranslationZ());
sharedElementBundle.putFloat(KEY_ELEVATION, view.getElevation());
// 忽略对ImageView的特殊处理
...
transitionArgs.putBundle(name, sharedElementBundle);
}

主要是遍历mSharedElements和mSharedElementNames通过captureSharedElementState(View, String, Bundle, Matrix, RectF)来更新每一个共享元素的State到Bundle中。最关键的就是将left、top、right、bottom参数放入bundle中传递给Activity B,然而这里的参数并不是通过View#getLeft()等方法拿到的,而是通过一些运算得到的

1
2
3
4
tempMatrix.reset();
view.transformMatrixToGlobal(tempMatrix);
tempBounds.set(0, 0, view.getWidth(), view.getHeight());
tempMatrix.mapRect(tempBounds);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// View.java
public void transformMatrixToGlobal(Matrix m) {
final ViewParent parent = mParent;
if (parent instanceof View) {
final View vp = (View) parent;
vp.transformMatrixToGlobal(m);
m.preTranslate(-vp.mScrollX, -vp.mScrollY);
} else if (parent instanceof ViewRootImpl) {
final ViewRootImpl vr = (ViewRootImpl) parent;
vr.transformMatrixToGlobal(m);
m.preTranslate(0, -vr.mCurScrollY);
}

m.preTranslate(mLeft, mTop);

// 这里将m前乘view.getMatrix是为了避免本身自带的Matrix造成的误差
if (!hasIdentityMatrix()) {
m.preConcat(getMatrix());
}
}

// ViewRootImpl.java
void transformMatrixToGlobal(Matrix m) {
m.preTranslate(mAttachInfo.mWindowLeft, mAttachInfo.mWindowTop);
}

我们查看View#transformMatrixToGlobal()方法可以看出内部是向上遍历调用Parent的transformMatrixToGlobal方法直到ViewRootImpl,然后将由Parent处理后的Matrix做preTranslate(mLeft, mTop)操作,而ViewRootImpl#transformMatrixToGlobal方法也是对Metrix做preTranslate,距离是当前Window在屏幕上的left/top,所以一般来说该方法返回的是所有Parent的preTranslate(mLeft, mTop)叠加结果,返回的Matrix为

$$ \left\{ \begin{matrix} 1 & 0 & leftOnScreen \\ 0 & 1 & topOnScreen \\ 0 & 0 & 1 \\ \end{matrix} \right\} $$

其中leftOnScreen和topOnScreen就是该View距离屏幕的左边/上边的距离。
但是真实情况返回Matrix的不一定是这样,因为View自身的Matrix可能并不是一个单位矩阵(设置过scale/rotation等),所以在拿到处理过的matrix后还需要做一次变换

1
2
3
if (!hasIdentityMatrix()) {
m.preConcat(getMatrix());
}

将这样处理后的Matrix作用于rect = [0, 0, view.getWidth(), view.getHeight()]的矩形就能够得到视觉上该View在屏幕上的坐标。

我们举个例子来说明这个方法,假设图中A代表屏幕,B为一个ViewGroup,左上角在A中的坐标为(100, 100),C长宽均为100,是B的child view,左上角在B中的坐标是(50, 50),同时C设置了transaltion,x、y均偏移25也就是图中灰色的区域(绿色区域为C的真实位置)。C的transformMatrixToGlobal方法中首先获取到Parent也就是B的Matrix,得到的是x、y偏移100的矩阵

$$ B= \left\{ \begin{matrix} 1 & 0 & 100 \\ 0 & 1 & 100 \\ 0 & 0 & 1 \\ \end{matrix} \right\} $$

随后将矩阵B偏移C在Parent中的left/top

$$ C= \left\{ \begin{matrix} 1 & 0 & 100 \\ 0 & 1 & 100 \\ 0 & 0 & 1 \\ \end{matrix} \right\} * \left\{ \begin{matrix} 1 & 0 & 50 \\ 0 & 1 & 50 \\ 0 & 0 & 1 \\ \end{matrix} \right\} = \left\{ \begin{matrix} 1 & 0 & 150 \\ 0 & 1 & 150 \\ 0 & 0 & 1 \\ \end{matrix} \right\} $$

因为C本身设置了translation,所以自身有一个非单位矩阵,将C乘以该矩阵得到最终的矩阵C2

$$ C2= \left\{ \begin{matrix} 1 & 0 & 150 \\ 0 & 1 & 150 \\ 0 & 0 & 1 \\ \end{matrix} \right\} * \left\{ \begin{matrix} 1 & 0 & 25 \\ 0 & 1 & 25 \\ 0 & 0 & 1 \\ \end{matrix} \right\} = \left\{ \begin{matrix} 1 & 0 & 175 \\ 0 & 1 & 175 \\ 0 & 0 & 1 \\ \end{matrix} \right\} $$

可以看出该矩阵的作用就是将坐标在x、y方向上移动175的距离,将该矩阵作用于长宽与C一样的矩形(0, 0, 100, 100)得到的就是我们看到的灰色区域,所以该方法最后得到的就是我们所能看到的View在屏幕上的坐标(而不是真正的坐标)。

看到这里大家一定有一个疑问,为什么不直接用getLocationOnScreen方法?其实这两种方法是有差别的。一方面getLocationOnScreen只能获得该View左上角在屏幕中的坐标而无法获得View右下角在屏幕中的坐标,另一方面如果View或者View的Parent设置了rotation,那么就会导致getLocationOnScreen方法获取不准确,因为getLocationOnScreen方法内部实现是针对点来做矩阵变换的,这样就会导致带有rotation的矩阵将左上角的点(0, 0)进行旋转,进而导致结果不准确。而transformMatrixToGlobal方法直接返回作用的Matrix,我们将该Matrix作用在矩形上,就可以获取准确的坐标。
总结:Activity A传递给Activity B的是视觉上Shared Views在屏幕中的坐标。

Activity B的是怎么处理Activity A传递的Bundle并实现动画

上文提到ExitTransitionCoordiantor的exit动画结束后会发送MSG_TAKE_SHARED_ELEMENTS给被启动Activity的EnterTransitionCoordinator通知它可以开始动画,在EnterTransitionCoordinator中将执行startSharedElementTransition方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// EnterTransitionCoordinator.java
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case MSG_TAKE_SHARED_ELEMENTS:
if (!mIsCanceled) {
mSharedElementsBundle = resultData;
onTakeSharedElements();
}
break;
...
}
}

private void onTakeSharedElements() {
if (!mIsReadyForTransition || mSharedElementsBundle == null) {
return;
}
final Bundle sharedElementState = mSharedElementsBundle;
mSharedElementsBundle = null;
OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
@Override
public void onSharedElementsReady() {
final View decorView = getDecor();
if (decorView != null) {
OneShotPreDrawListener.add(decorView, false, () -> {
startTransition(() -> {
// 将Activity A传递过来的带有View参数的bundle传递,并开始动画
startSharedElementTransition(sharedElementState);
});
});
decorView.invalidate();
}
}
};
if (mListener == null) {
listener.onSharedElementsReady();
} else {
mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
}
}

我们着重关注startSharedElementTransition方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// EnterTransitionCoordinator.java
private void startSharedElementTransition(Bundle sharedElementState) {
...
// Now start shared element transition
ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
mSharedElementNames);
showViews(mSharedElements, true);
scheduleSetSharedElementEnd(sharedElementSnapshots);
// 将Activity B上的Shared Views设置为Activity A上的初始状态,并返回在Activity B中的原始状态
ArrayList<SharedElementOriginalState> originalImageViewState =
setSharedElementState(sharedElementState, sharedElementSnapshots);
requestLayoutForSharedElements();
...
setGhostVisibility(View.INVISIBLE);
scheduleGhostVisibilityChange(View.INVISIBLE);
pauseInput();
// 开始动画
Transition transition = beginTransition(decorView, startEnterTransition,
startSharedElementTransition);
scheduleGhostVisibilityChange(View.VISIBLE);
setGhostVisibility(View.VISIBLE);
...
// 将Views设置为原始状态,触发Transition 捕获起始状态&创建/执行动画
setOriginalSharedElementState(mSharedElements, originalImageViewState);
...
}

调整Shared Views为Activity A上的初始状态

从上面的代码可以看到首先调用了setSharedElementState方法,该方法将Shared Views通过传递来的bundle调整为初始状态,返回的originalImageViewState是Shared Views在Activity B中的原始状态,也就是动画的结束状态,用于后续的恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// EnterTransitionCoordinator.java
protected ArrayList<SharedElementOriginalState> setSharedElementState(
Bundle sharedElementState, final ArrayList<View> snapshots) {
ArrayList<SharedElementOriginalState> originalImageState =
new ArrayList<SharedElementOriginalState>();
if (sharedElementState != null) {
Matrix tempMatrix = new Matrix();
RectF tempRect = new RectF();
final int numSharedElements = mSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View sharedElement = mSharedElements.get(i);
String name = mSharedElementNames.get(i);
SharedElementOriginalState originalState = getOldSharedElementState(sharedElement,
name, sharedElementState);
originalImageState.add(originalState);
setSharedElementState(sharedElement, name, sharedElementState,
tempMatrix, tempRect, null);
}
}
if (mListener != null) {
mListener.onSharedElementStart(mSharedElementNames, mSharedElements, snapshots);
}
return originalImageState;
}

方法内部其实就是遍历所有的Shared View,针对每个View先获取oldState保存,然后设置为动画初始状态。在getOldSharedElementState内部只是简单的保存了left/top等信息,我们着重看一下将View设置为初始状态的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// EnterTransitionCoordinator.java
private void setSharedElementState(View view, String name, Bundle transitionArgs,
Matrix tempMatrix, RectF tempRect, int[] decorLoc) {
// 这里省略针对ImageView的特殊处理
...
float left = sharedElementBundle.getFloat(KEY_SCREEN_LEFT);
float top = sharedElementBundle.getFloat(KEY_SCREEN_TOP);
float right = sharedElementBundle.getFloat(KEY_SCREEN_RIGHT);
float bottom = sharedElementBundle.getFloat(KEY_SCREEN_BOTTOM);

if (decorLoc != null) {
left -= decorLoc[0];
top -= decorLoc[1];
right -= decorLoc[0];
bottom -= decorLoc[1];
} else {
// Find the location in the view's parent
getSharedElementParentMatrix(view, tempMatrix);
tempRect.set(left, top, right, bottom);
tempMatrix.mapRect(tempRect);

float leftInParent = tempRect.left;
float topInParent = tempRect.top;

// Find the size of the view
view.getInverseMatrix().mapRect(tempRect);
float width = tempRect.width();
float height = tempRect.height();

// Now determine the offset due to view transform:
view.setLeft(0);
view.setTop(0);
view.setRight(Math.round(width));
view.setBottom(Math.round(height));
tempRect.set(0, 0, width, height);
view.getMatrix().mapRect(tempRect);

left = leftInParent - tempRect.left;
top = topInParent - tempRect.top;
right = left + width;
bottom = top + height;
}

int x = Math.round(left);
int y = Math.round(top);
int width = Math.round(right) - x;
int height = Math.round(bottom) - y;
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);

view.layout(x, y, x + width, y + height);
}

这里又涉及到一些矩阵知识了,首先是拿到Activity A传过来的View的left/top/right/bottom参数(在屏幕上的),这里我们是不能直接设置给Activity B的View,因为Activity B的View依赖于它的Parent,所以我们首先将屏幕上的坐标转换为在Parent中的坐标,通过getSharedElementParentMatrix(view, tempMatrix)来拿到转换的Matrix对tempRect进行转换,我们看看getSharedElementParentMatrix的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// EnterTransitionCoordinator.java
private void getSharedElementParentMatrix(View view, Matrix matrix) {
final int index = mSharedElementParentMatrices == null ? -1
: mSharedElements.indexOf(view);
if (index < 0) {
matrix.reset();
ViewParent viewParent = view.getParent();
if (viewParent instanceof ViewGroup) {
// Find the location in the view's parent
ViewGroup parent = (ViewGroup) viewParent;
parent.transformMatrixToLocal(matrix);
matrix.postTranslate(parent.getScrollX(), parent.getScrollY());
}
} else {
// The indices of mSharedElementParentMatrices matches the
// mSharedElement matrices.
Matrix parentMatrix = mSharedElementParentMatrices.get(index);
matrix.set(parentMatrix);
}
}

通过调用ParenttransformMatrixToLocal方法来获得转换矩阵,这和之前提到的transformMatrixToGlobal方法实现非常相似,这里说结论,transformMatrixToLocal返回的Matrix和transformMatrixToGlobal相反,一般返回的Matrix如下所示(除去View自身Matrix的影响的话)

$$ \left\{ \begin{matrix} 1 & 0 & -leftOnScreen \\ 0 & 1 & -topOnScreen \\ 0 & 0 & 1 \\ \end{matrix} \right\} $$

其中leftOnScreen和topOnScreen就是该Parent距离屏幕的左/上的距离。经过此Matrix转换后tempRect的坐标就是我们所期望的View在Parent中的坐标了,但是仍然不能直接设置给View!如果假设View本身设置过Scale=2,那么当将计算后的坐标设置给View时会在正常大小上再作用一个Scale=2,显示的效果就是预期的两倍,所以我们还需要以下操作:

  1. 计算我们需要设置的真正width/height:通过拿到View的逆矩阵应用到tempRect上可以得到我们实际需要设置的width/height
  2. 计算真正的left/top:之前我们已经计算出视觉上我们的View在Parent中的left和top,但是因为View自身的Matrix影响,我们需要计算出该矩阵应用于矩形后会导致矩形的left/top相比原来偏移多少,然后在设置left/top的时候减去这个偏差

调整Shared Views为最终状态

共享元素动画使用的是Transition框架,我们只要调整View的状态,就可以自动捕获初始状态/结束状态来生成动画,所以后面要做的就是将SharedViews调整成最初的状态(也就是在Activity B中的最终状态)。从上面的startSharedElementTransition实现来看,首先调用beginTransition方法,内部调用了TransitionManager.beginDelayedTransition(decorView, transition)来开始动画,随后调用setOriginalSharedElementState(mSharedElements, originalImageViewState)来将Shared Views设置为最终状态,我们来看下这个方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// EnterTransitionCoordinator.java
protected static void setOriginalSharedElementState(ArrayList<View> sharedElements,
ArrayList<SharedElementOriginalState> originalState) {
for (int i = 0; i < originalState.size(); i++) {
View view = sharedElements.get(i);
SharedElementOriginalState state = originalState.get(i);
// 忽略对ImageView的特殊处理
...
view.setElevation(state.mElevation);
view.setTranslationZ(state.mTranslationZ);
int widthSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredWidth,
View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredHeight,
View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(state.mLeft, state.mTop, state.mRight, state.mBottom);
}
}

originState就是之前调用setSharedElementState返回的原始状态,我们就是通过这个state来恢复View的状态的,实现就只是简单的measure、layout。

参与动画的元素是怎么能够保证不被遮挡的

看到这里其实可以知道一点,参与动画的元素都是Activity B上的View,从A过渡过来的效果不过是将B上的View进行转换而已,但是B上的View都是依赖于它的Parent,而动画的初始状态的位置又不能保证在该Parent的可视区域内,按照正常流程,Shared Views很大概率是会在动画过程中移动到不可见区域导致View不可见,所以我们需要将Shared Views移动到屏幕顶层,也就是ViewGroupOverlay层。

GhostView

我们直接使用ViewGroupOverlay会有一个问题,ViewGroupOverlay#add(View v)方法会将view从原有的Parent中remove,再添加到ViewGroupOverlay中,可是我们并不希望改变View原有的层级结构,毕竟动画结束后所有View需要恢复原样,所以Android提供了GhostView对ViewOverlay进行了封装,通过调用GhostViet#addGhost方法来将View添加到ViewOverlay层,且能够保持原来的View不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// GhostView.java
@Override
protected void onDraw(Canvas canvas) {
if (canvas instanceof DisplayListCanvas) {
DisplayListCanvas dlCanvas = (DisplayListCanvas) canvas;
mView.mRecreateDisplayList = true;
RenderNode renderNode = mView.updateDisplayListIfDirty();
if (renderNode.isValid()) {
dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode
dlCanvas.drawRenderNode(renderNode);
dlCanvas.insertInorderBarrier(); // re-disable reordering/shadows
}
}
}

其实GhostView添加到ViewOverlay的并不是原来的View,而是自己创建的FrameLayout,然后在绘制的时候获取View的renderNode来对自己的canvas进行绘制,达到视觉上和View一摸一样的目的,并且每次View有改变都会通知到GhostView来绘制

使用GhostView后如何保证添加到ViewGroupOverlay的元素与不添加之前层级保持一致

我们使用GhostView的时候是将各个View加到Overlay上,但是GhostView是怎么保证View在视觉上的顺序和View在正常布局中的顺序一致的呢?我们发现GhostView#add方法中这么两行代码

1
2
int firstGhost = moveGhostViewsToTop(overlay.mOverlayViewGroup, tempViews);
insertIntoOverlay(overlay.mOverlayViewGroup, parent, ghostView, tempViews, firstGhost);

由于获得的ViewOverlay中可能存在其他地方加进来的View,所以首先通moveGhostViewsToTop方法遍历其中所有的View,然后将所有GhostView重新add到ViewOverlay中保证GhostView覆盖在最上层。随后就是真正的add操作了,insertIntoOverlay内部其实是通过二分法来找到View插入的index,判断依据是View是否会被绘制在被比较View的上方,通过isOnTop方法来判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// GhostView.java

// 通过两个View的Paren队列来判断前者是否在后者上方
private static boolean isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith) {
if (viewParents.isEmpty() || comparedWith.isEmpty() ||
viewParents.get(0) != comparedWith.get(0)) {
// Not the same decorView -- arbitrary ordering
return true;
}
int depth = Math.min(viewParents.size(), comparedWith.size());
for (int i = 1; i < depth; i++) {
View viewParent = viewParents.get(i);
View comparedWithParent = comparedWith.get(i);

if (viewParent != comparedWithParent) {
// i - 1 is the same parent, but these are different children.
// 走到这说明两者在同一个View的层级下,那么根据在Parent中的顺序来判断
return isOnTop(viewParent, comparedWithParent);
}
}

// 走到这说明两者截取长度为depth的Parent队列一摸一样,
// 说明真实队列较长的View是另一个View的Child(或者是另一个View同级的View的child),也就说明在屏幕上更加靠前
// one of these is the parent of the other
boolean isComparedWithTheParent = (comparedWith.size() == depth);
return isComparedWithTheParent;
}

具体实现是将两个View的Parent队列(包括本身)做比较,首先将较长队列截取为长度和较短队列一致,然后遍历比较,会出现以下两种情况

  • 截取后的两个队列一摸一样:那么较长的队列的View必然是另一队列View的child,自然比parent更加靠上
  • 截取后的两个队列在某一节点开始不一样:那么只要判断这两个节点的层级就可以了,通过调用isOnTop(View, View)来判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static boolean isOnTop(View view, View comparedWith) {
ViewGroup parent = (ViewGroup) view.getParent();

final int childrenCount = parent.getChildCount();
final ArrayList<View> preorderedList = parent.buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& parent.isChildrenDrawingOrderEnabled();

// This default value shouldn't be used because both view and comparedWith
// should be in the list. If there is an error, then just return an arbitrary
// view is on top.
boolean isOnTop = true;
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? parent.getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? parent.getChildAt(childIndex) : preorderedList.get(childIndex);
if (child == view) {
isOnTop = false;
break;
} else if (child == comparedWith) {
isOnTop = true;
break;
}
}

if (preorderedList != null) {
preorderedList.clear();
}
return isOnTop;
}

该方法判断两个View绘制层级的逻辑和ViewGroup#dispatchDraw是一样的,有兴趣的同学可以深入研究一下。

动画过程中其余元素的状态如何

上文提到除去Shared Views其余可见性为Visible的View都会被添加到mTransitioningViews中,本节我们看看动画过程中这些View的状态是怎么样的。我们从EnterTransitionCoordinator#viewsReady()方法入手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// EnterTransitionCoordinator.java
@Override
protected void viewsReady(ArrayMap<String, View> sharedElements) {
super.viewsReady(sharedElements);
mIsReadyForTransition = true;
hideViews(mSharedElements);
Transition viewsTransition = getViewsTransition();
if (viewsTransition != null && mTransitioningViews != null) {
removeExcludedViews(viewsTransition, mTransitioningViews);
stripOffscreenViews();
hideViews(mTransitioningViews);
}
if (mIsReturning) {
sendSharedElementDestination();
} else {
moveSharedElementsToOverlay();
}
if (mSharedElementsBundle != null) {
onTakeSharedElements();
}
}

从实现可以看出首先将mTransitioningViews隐藏,将view的alpha设置为0。真正进行动画是在startSharedElementTransition方法中进行,其中调用了beginTransition方法开始真正的动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// EnterTransitionCoordinator.java
private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
boolean startSharedElementTransition) {
Transition sharedElementTransition = null;
// 忽略共享元素动画
...
Transition viewsTransition = null;
if (startEnterTransition) {
mIsViewsTransitionStarted = true;
if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
viewsTransition = configureTransition(getViewsTransition(), true);
}
if (viewsTransition == null) {
viewsTransitionComplete();
} else {
final ArrayList<View> transitioningViews = mTransitioningViews;
viewsTransition.addListener(new ContinueTransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
mEnterViewsTransition = transition;
if (transitioningViews != null) {
showViews(transitioningViews, false);
}
super.onTransitionStart(transition);
}

@Override
public void onTransitionEnd(Transition transition) {
mEnterViewsTransition = null;
transition.removeListener(this);
viewsTransitionComplete();
super.onTransitionEnd(transition);
}
});
}
}

Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
if (transition != null) {
transition.addListener(new ContinueTransitionListener());
if (startEnterTransition) {
setTransitioningViewsVisiblity(View.INVISIBLE, false);
}
TransitionManager.beginDelayedTransition(decorView, transition);
if (startEnterTransition) {
setTransitioningViewsVisiblity(View.VISIBLE, false);
}
decorView.invalidate();
} else {
transitionStarted();
}
return transition;
}

可以看出其余的View的动画是通过getEnterTransition获得的,和Shared Transition同时进行,但是该方法传入了一个参数来判断是否进行enterTransition,该参数在startSharedElementTransition获得

1
2
3
4
5
6
7
// EnterTransitionCoordinator.java
boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;

private boolean allowOverlappingTransitions() {
return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
: getWindow().getAllowEnterTransitionOverlap();
}

通过window#getAllowEnterTransitionOverlap()来判断是否进行enterTransition,该方法表示是否允许enterTransition尽可能早的执行,如果为True将和Shared Transition一起进行,如果为false呢?那么就会在EnterTransitionCoordinator#onRemoteExitTransitionComplete方法中被触发执行,该方法是被ExitTransitionCoordiantor通过notifyComplete触发的

1
2
3
4
5
6
protected void notifyComplete() {
...
mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
notifyExitComplete();
...
}

可见在EnterTransitionCoordinator执行Shared Transition动画后立马触发EnterTransition的动画(如果EnterTransitionOverlap为false),视觉上差别不会很大

默认的动画是在哪里设置的

无论是共享元素动画还是EnterTransition都是从Window中获取的,我们并没有手动设置也可以生效,那是因为Window在加载的时候默认加载了动画

1
2
3
4
5
6
7
8
// PhoneWindow.java
private void installDecor() {
...
mEnterTransition = getTransition(mEnterTransition, null, R.styleable.Window_windowEnterTransition);
...
mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null, R.styleable.Window_windowSharedElementEnterTransition);
...
}

可见是通过windowEnterTransition/windowSharedElementEnterTransition来获取的,在sdk/platforms/android-28/data/res/values/themes_material.xml文件中找到定义

1
2
3
4
5
6
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<changeTransform/>
<changeClipBounds/>
<changeImageTransform/>
</transitionSet>

可以看出默认的Shared Transition是四个Transition的集合,其中默认支持ImageView的tranform。

其余元素在动画过程中被Shared Views覆盖怎么办

从最开始的Demo 录屏可以看出Activity B左下角的Button展示后会被参与共享元素动画的背景封面遮挡,动画结束后才再次显示出来,这是因为Shared Views都被显示到ViewGroupOverlay中了,所以会覆盖Button,动画结束后回到正常的View层级中Button才能显示。为了解决这个问题我们可以将Button也加入到共享元素中去,这样Button也会显示到ViewOverlay中,就不会被覆盖了。

怎么将View加入到共享元素中去

如果我们使用之前的办法,给Button加上transitionName,会发现行不通,因为在收集Shared View的时候会针对Activity A传递过来的name来做去重

1
2
3
4
5
6
7
8
9
// EnterTransitionCoordinator.java
protected void viewsReady(ArrayMap<String, View> sharedElements) {
// 将sharedElements中不存在于mAllSharedElementNames中的元素删除
sharedElements.retainAll(mAllSharedElementNames);
if (mListener != null) {
mListener.onMapSharedElements(mAllSharedElementNames, sharedElements);
}
...
}

但是我们可以发现后面又调用了mListener#onMapSharedElements方法来添加Shared Views,该Listener类型为SharedElementCallback,我们可以通过activity来设置Listener并重写onMapSharedElements方法

1
2
3
4
5
6
7
8
9
10
setEnterSharedElementCallback(object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>?,
sharedElements: MutableMap<String, View>?
) {
super.onMapSharedElements(names, sharedElements)
// 将Button加入SharedViews,避免被遮盖
sharedElements?.put("button", button)
}
})

这样就可以达到将Button加入到SharedElement中去的目的了,viewsReady中随后会调用moveSharedElementsToOverlay()将SharedView显示到ViewOverlay中去。

Button会参与共享元素动画吗

结论是不会,它只会在共享元素动画结束后和其余View一样通过调用moveSharedElementsFromOverlay()返回到正常布局中去。通过之前的分析可以知道是通过setSharedElementState方法将View设置为初始状态的,在该方法中首先通过transitinName从Activity A传递过来的bundle中获取参数,如果获取不到就直接return。Button是我们强行加入的,Activity A传递的bundle中自然没有,所以Button不会做任何改变,效果如下

返回动画的流程

返回和进入的核心流程很相似,但是更加简单。首先我们需要调用Activity#finishAfterTransition()方法来触发返回的动画,其内部调用了ActivityTransitionState#startExitBackTransition()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean startExitBackTransition(final Activity activity) {
if (mEnteringNames == null || mCalledExitCoordinator != null) {
return false;
} else {
if (!mHasExited) {
...
mReturnExitCoordinator = new ExitTransitionCoordinator(activity,
activity.getWindow(), activity.mEnterTransitionListener, mEnteringNames,
null, null, true);
...
if (delayExitBack && decor != null) {
final ViewGroup finalDecor = decor;
OneShotPreDrawListener.add(decor, () -> {
if (mReturnExitCoordinator != null) {
mReturnExitCoordinator.startExit(activity.mResultCode,
activity.mResultData);
}
});
} else {
mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData);
}
}
return true;
}
}

内部给Activity B创建一个ExitTransitionCoordinator,调用startExit方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void startExit(int resultCode, Intent data) {
if (!mIsExitStarted) {
mIsExitStarted = true;
pauseInput();
...
moveSharedElementsToOverlay();
if (decorView != null && decorView.getBackground() == null) {
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
final boolean targetsM = decorView == null || decorView.getContext()
.getApplicationInfo().targetSdkVersion >= VERSION_CODES.M;
ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames :
mAllSharedElementNames;
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this,
sharedElementNames, resultCode, data);
mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() {
@Override
public void onTranslucentConversionComplete(boolean drawComplete) {
if (!mIsCanceled) {
fadeOutBackground();
}
}
}, options);
startTransition(new Runnable() {
@Override
public void run() {
startExitTransition();
}
});
}
}

通过调用Activity#convertToTranslucent()触发Activity A restart()接收到options,options是通过ActivityOptions#makeSceneTransitionAnimation方法创建,将this传递,并设置isReturning=true。Activity A触发后走的和之前一样的流程到EnterTransitionCoordinator#viewsReady()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void viewsReady(ArrayMap<String, View> sharedElements) {
super.viewsReady(sharedElements);
mIsReadyForTransition = true;
hideViews(mSharedElements);
Transition viewsTransition = getViewsTransition();
if (viewsTransition != null && mTransitioningViews != null) {
removeExcludedViews(viewsTransition, mTransitioningViews);
stripOffscreenViews();
hideViews(mTransitioningViews);
}
if (mIsReturning) {
sendSharedElementDestination();
} else {
moveSharedElementsToOverlay();
}
if (mSharedElementsBundle != null) {
onTakeSharedElements();
}
}

不过由于returning为true,触发sendSharedElementDestination方法,将MSG_SHARED_ELEMENT_DESTINATION发送给Activity B的ExitTransitionCoordinator,ExitTransitionCoordinator开始sharedElementExitBack方法开始返回动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ExitTransitionCoordinator.java
private void sharedElementExitBack() {
final ViewGroup decorView = getDecor();
if (decorView != null) {
decorView.suppressLayout(true);
}
if (decorView != null && mExitSharedElementBundle != null &&
!mExitSharedElementBundle.isEmpty() &&
!mSharedElements.isEmpty() && getSharedElementTransition() != null) {
startTransition(new Runnable() {
public void run() {
startSharedElementExit(decorView);
}
});
} else {
sharedElementTransitionComplete();
}
}

private void startSharedElementExit(final ViewGroup decorView) {
Transition transition = getSharedElementExitTransition();
transition.addListener(new TransitionListenerAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
transition.removeListener(this);
if (isViewsTransitionComplete()) {
delayCancel();
}
}
});
final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle,
mSharedElementNames);
OneShotPreDrawListener.add(decorView, () -> {
// preDraw的时候将View设置为Activity A的状态,来触发动画
setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots);
});
setGhostVisibility(View.INVISIBLE);
scheduleGhostVisibilityChange(View.INVISIBLE);
if (mListener != null) {
mListener.onSharedElementEnd(mSharedElementNames, mSharedElements,
sharedElementSnapshots);
}
// 开始动画
TransitionManager.beginDelayedTransition(decorView, transition);
scheduleGhostVisibilityChange(View.VISIBLE);
setGhostVisibility(View.VISIBLE);
decorView.invalidate();
}

结语

本文大致介绍了共享元素动画的流程,且着重解析了底层实现的细节,如果大家想深挖动画流程建议阅读源码。

参考资料

Android中ResultReceiver使用
Android高阶转场动画-ShareElement完全攻略
安卓自定义View进阶-Matrix原理
安卓自定义View进阶-Matrix详解
GhostView
ViewOverlay 的使用