Andriod 5.0及之后开始支持Activity之间的共享元素动画,共享元素即启动Activity的时候,Activity A中的控件能够通过动画的方式流畅的过渡到Activity B中对应的控件。 之前在项目中用到了共享元素动画,遇到了一些坑,碍于不了解底层原理&时间紧迫,实现的效果并不完美。最近得空带着一些疑惑阅读了一波源码,本文将带着这些问题来一步步解析源码,下图为实现效果。
PS:本文源码基于android-28
使用共享元素动画 使用共享元素动画很简单,只需要三步
通过给两个Activity设定Window.FEATURE_CONTENT_TRANSITIONS来启用transition api
给两个Activity对应的控件加上transitionName属性,且对应控件的transitionName应该保持一致
使用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 supportPostponeEnterTransition() 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动画流程如下所示:
ExitTransitionCoordinator在ActivityOptions#makeSceneTransitionAnimation方法中被创建,并将它传入options中返回
Activity#startActivity最终调用到cancelInputsAndStartExitTransition()触发ExitTransitionCoordinator#startExit(),隐藏其余的View同时将SharedViews移动到顶层
Activity B启动,通过ActivityTransitionState#enterReady来创建EnterTransitionCoordiantor,同时调用它的startEnter()做一些准备操作
MSG_SET_REMOTE_RECEIVER被发送给ExitTransitionCoordinator来设置相互的引用
将Window设置为透明
Window的background设置为alpha=0
将不参与SharedTransition的View和SharedViews设置为alpha=0
将SharedViews移动到顶层
ExitTransitionCoordinator的exit动画结束后发送MSG_TAKE_SHARED_ELEMENTS给EnterTransitionCoordinator,EnterTransitionCoordinator开始动画
SharedViews设置为alpha=1
SharedView的位置和大小被设置为启动Activity对应Views的原始状态
开始共享元素动画
发送MSG_HIDE_SHARED_ELEMENTS给ExitTransitionCoordinator通知启动Activity隐藏对应的SharedViews
ExitTransitionCoordinator同时发送MSG_EXIT_TRANSITION_COMPLETE给EnterTransitionCoordinator,通知它可以开始EnterTransition动画(不是SharedEnterTransitino)
动画结束后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 protected ArrayMap<String, View> mapSharedElements (ArrayList<String> accepted, ArrayList<View> localViews) { ... if (decorView != null ) { decorView.findNamedViews(sharedElements); } ... return sharedElements; } protected void viewsReady (ArrayMap<String, View> sharedElements) { sharedElements.retainAll(mAllSharedElementNames); if (mListener != null ) { mListener.onMapSharedElements(mAllSharedElementNames, sharedElements); } setSharedElements(sharedElements); ... if (decorView != null ) { 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.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 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()); ... 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 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); if (!hasIdentityMatrix()) { m.preConcat(getMatrix()); } } 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 @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(() -> { 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 private void startSharedElementTransition (Bundle sharedElementState) { ... ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, mSharedElementNames); showViews(mSharedElements, true ); scheduleSetSharedElementEnd(sharedElementSnapshots); 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); ... 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 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 private void setSharedElementState (View view, String name, Bundle transitionArgs, Matrix tempMatrix, RectF tempRect, int [] decorLoc) { ... 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 { getSharedElementParentMatrix(view, tempMatrix); tempRect.set(left, top, right, bottom); tempMatrix.mapRect(tempRect); float leftInParent = tempRect.left; float topInParent = tempRect.top; view.getInverseMatrix().mapRect(tempRect); float width = tempRect.width(); float height = tempRect.height(); 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 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) { ViewGroup parent = (ViewGroup) viewParent; parent.transformMatrixToLocal(matrix); matrix.postTranslate(parent.getScrollX(), parent.getScrollY()); } } else { Matrix parentMatrix = mSharedElementParentMatrices.get(index); matrix.set(parentMatrix); } }
通过调用Parent 的transformMatrixToLocal方法来获得转换矩阵,这和之前提到的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,显示的效果就是预期的两倍,所以我们还需要以下操作:
计算我们需要设置的真正width/height:通过拿到View的逆矩阵应用到tempRect上可以得到我们实际需要设置的width/height
计算真正的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 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); ... 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 @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(); dlCanvas.drawRenderNode(renderNode); dlCanvas.insertInorderBarrier(); } } }
其实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 private static boolean isOnTop (ArrayList<View> viewParents, ArrayList<View> comparedWith) { if (viewParents.isEmpty() || comparedWith.isEmpty() || viewParents.get(0 ) != comparedWith.get(0 )) { 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) { return isOnTop(viewParent, comparedWithParent); } } 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(); 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 @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 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 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 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 protected void viewsReady (ArrayMap<String, View> sharedElements) { 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) sharedElements?.put("button" , button) } })
这样就可以达到将Button加入到SharedElement中去的目的了,viewsReady中随后会调用moveSharedElementsToOverlay()将SharedView显示到ViewOverlay中去。
结论是不会,它只会在共享元素动画结束后和其余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 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, () -> { 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 的使用