Android 转场动画源码剖析

Posted 不会写代码的丝丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 转场动画源码剖析相关的知识,希望对你有一定的参考价值。

前言

= =时隔一年 我竟为了一个UI效果再次学习Android,具体原因:UI设计一个转场动画中非共享元素也要执行动画。

转场动画基础使用可参阅官方文档.本文主要描述Activity转场中Google的设计与实现(Fragment比较简单不做讨论)。

我们看下本期的案例:

具体效果

首先我们需要知道MainActivity需要执行退场动画,SecondActivity执行入场动画,两个Activity需要透传共享元素信息,但是Activity可能存在一个问题:跨进程.

因此我们需要自己去设计一套流程在去参看google 源码会更好理解

我们这里直接给出一个相关生命周期回调

MainActivity: onCreate
MainActivity: onStart
MainActivity: onResume
MainActivity: 点击执行转场动画
MainActivity: onMapSharedElements
MainActivity: captureStartValues
MainActivity: captureStartValues
MainActivity: captureEndValues
MainActivity: captureEndValues
MainActivity: createAnimator
MainActivity: onStart进行动画 
MainActivity: onPuase
SecondActivity: onCreate
SecondActivity: onStart
SecondActivity: onResume
SecondActivity: onMapSharedElements
MainActivity: onEnd进行动画 
MainActivity: onCaptureSharedElementSnapshot 
MainActivity: onSharedElementsArrived
SecondActivity: onSharedElementsArrived
SecondActivity: onRejectSharedElements []
SecondActivity: onCreateSnapshotView
SecondActivity: onCreateSnapshotView
SecondActivity: onSharedElementStart
SecondActivity: captureStartValues 
SecondActivity: captureStartValues 
SecondActivity: onSharedElementEnd
SecondActivity: captureEndValues 
SecondActivity: captureEndValues 
SecondActivity: createAnimator btnTransition
SecondActivity: onStart进行动画
SecondActivity: onStart进行动画
SecondActivity: onEnd进行动画
SecondActivity: onEnd进行动画
MainActivity: onStop

转场设计理念

我们假设MainActivity跳转至SecondActivity进行共享元素动画。

步骤1

我们MainActivity需要收集所有的需要执行的共享元素view的信息并为其赋值一个ID方便在SecondActivity中对比执行动画。

这个步骤对应Google设计代码中如下代码

 val intent = Intent(this, SecondActivity::class.java)
            val options = ActivityOptions
                .makeSceneTransitionAnimation(
                    this,
                    //设置一个共享元素,并赋值一个IDivAvatar
                    android.util.Pair<View, String>(binding.ivAvatar, "ivAvatar"),
                    //设置一个共享元素,并赋值一个btnTransition
                    android.util.Pair<View, String>(binding.btnTransition, "btnTransition")
                )
            startActivity(intent, options.toBundle())

我们共享元素的ID其实有一个名字叫transitionName,这个是一个View属性您可以在代码或者XML赋值。

步骤2

当用户声明MainActivity中的共享元素后我们边开始收集当前视图的一些信息。并提供一个回调允许开发则再次过滤掉一些不必要的共享元素,执行View信息的收集。

首先被回调onMapSharedElements函数,你可以在这个函数清楚某个共享元素
ExitSharedElementCallback->SharedElementCallback->onMapSharedElements

//SharedElementCallback
 override fun onMapSharedElements(
						//所有共享元素transitionName集合
						names: MutableList<String>,
						//Key是transitionName Value是View
					    sharedElements: MutableMap<String, View>) 
                super.onMapSharedElements(names, sharedElements)
                //你可以操作sharedElements 删减贡献元素的数量
 

在过滤需要真正执行动画元素后开始调用退出动画的TransitioncaptureStartValuescaptureEndValues函数用于捕获需要执行退出动画的元素。

 //为退出动画执行收集view信息
override fun captureEndValues(transitionValues: android.transition.TransitionValues?) 
            super.captureEndValues(transitionValues)
           
        

 //为退出动画执行收集view信息       
override fun captureStartValues(transitionValues: android.transition.TransitionValues?) 
            super.captureStartValues(transitionValues)
           

        

步骤3

当收集足够的退出动画信息收开始执行Transition#createAnimator创建动画并运行

        override fun createAnimator(
            sceneRoot: ViewGroup?,
            startValues: android.transition.TransitionValues?,
            endValues: android.transition.TransitionValues
        ): Animator  
            return super.createAnimator(sceneRoot, startValues, endValues)
        

步骤4

此时虽然正在进行退出动画但是可以传递共享元素信息给而给界面SecondActivity,此时回调setEnterSharedElementCallback ->SharedElementCallback#onMapSharedElements

  override fun onMapSharedElements(
  				//上个界面MainActivity传入的共享元素名称transitionName
                names: MutableList<String>,
                //这里Key名称是共享元素名称transitionName,
                //value是SeconActivity同transitionName对应View
                //注意这里的View是SeconActivity的,如果没有transitionName在SeconActivity没有对应的View
                //那么transitionName会回调到onRejectSharedElements
                //此时你需要做SeconActivity没有View共享元素动画的话,可以在这个回调构建一个View(一定要添加到父布局中否则一样会判断到onRejectSharedElements)并放入sharedElements这个Map中
                //因为可能跨程所以View不可能是上个界面的
                sharedElements: MutableMap<String, View>
            ) 
                super.onMapSharedElements(names, sharedElements)
            

步骤5 共享元素动画信息传递

MainActivitySecondActivity都需要等MainActivity退出动画完成后才能继续执行。
假设退出动画完成,那么此时当前界面共享元素信息进行捕获给SecondActivity。(退出动画可能影响共享元素位置,如共享元素参与动画)

捕获的信息会回调到onCaptureSharedElementSnapshot
这个onCaptureSharedElementSnapshot主要用于传入当前View样子的Bitmap。

 override fun onCaptureSharedElementSnapshot(
                sharedElement: View,
                viewToGlobalMatrix: Matrix?,
                screenBounds: RectF?
            ): Parcelable 
              return super.onCaptureSharedElementSnapshot(
                    sharedElement,
                    viewToGlobalMatrix,
                    screenBounds
                )
            

我们参考父类onCaptureSharedElementSnapshot

  public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix,
            RectF screenBounds) 
       //如果是ImageView,那么保存缩放设置和Bitmap到Bundle中
        if (sharedElement instanceof ImageView) 
            ImageView imageView = ((ImageView) sharedElement);
            Drawable d = imageView.getDrawable();
            Drawable bg = imageView.getBackground();
            if (d != null && bg == null) 
                Bitmap bitmap = createDrawableBitmap(d);
                if (bitmap != null) 
                    Bundle bundle = new Bundle();
                    bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap);
                    bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE,
                            imageView.getScaleType().toString());
                    if (imageView.getScaleType() == ScaleType.MATRIX) 
                        Matrix matrix = imageView.getImageMatrix();
                        float[] values = new float[9];
                        matrix.getValues(values);
                        bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values);
                    
                    return bundle;
                
            
        
        //如果是非ImageView,那么你会得到当前视图的快照Bitmap方便你在第二个界面生产一个一模一样的View
        int bitmapWidth = Math.round(screenBounds.width());
        int bitmapHeight = Math.round(screenBounds.height());
        Bitmap bitmap = null;
        if (bitmapWidth > 0 && bitmapHeight > 0) 
            float scale = Math.min(1f, ((float) MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight));
            bitmapWidth = (int) (bitmapWidth * scale);
            bitmapHeight = (int) (bitmapHeight * scale);
            if (mTempMatrix == null) 
                mTempMatrix = new Matrix();
            
            mTempMatrix.set(viewToGlobalMatrix);
            mTempMatrix.postTranslate(-screenBounds.left, -screenBounds.top);
            mTempMatrix.postScale(scale, scale);
            bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            canvas.concat(mTempMatrix);
            sharedElement.draw(canvas);
        
        return bitmap;
    

onCaptureSharedElementSnapshot 你会发现并没有先关位置信息捕获,那么第二个界面怎么获取上个界面共享元素屏幕位置呢?

因为位置信息是共享动画非常必要的因此Android 已经帮我捕获了,并且是在调用onCaptureSharedElementSnapshot前捕获的。具体ActivityTransitionCoordinatorcaptureSharedElementState函数中

class ActivityTransitionCoordinator

//...其他代码略
 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());

        Parcelable bitmap = null;
        if (mListener != null) 
        	//这里就是我们的生产快照代码
            bitmap = mListener.onCaptureSharedElementSnapshot(view, tempMatrix, tempBounds);
        

        if (bitmap != null) 
            sharedElementBundle.putParcelable(KEY_SNAPSHOT, bitmap);
        

        if (view instanceof ImageView) 
            ImageView imageView = (ImageView) view;
            int scaleTypeInt = scaleTypeToInt(imageView.getScaleType());
            sharedElementBundle.putInt(KEY_SCALE_TYPE, scaleTypeInt);
            if (imageView.getScaleType() == ImageView.ScaleType.MATRIX) 
                float[] matrix = new float[9];
                imageView.getImageMatrix().getValues(matrix);
                sharedElementBundle.putFloatArray(KEY_IMAGE_MATRIX, matrix);
            
        

        transitionArgs.putBundle(name, sharedElementBundle);
    

ActivityTransitionCoordinator主要用于IPC通信协调两个进程的Activity完成动画以及GhostView环境制造(你有考虑过我们做转场动画如果动画超过父View范围问题吗?我们每次执行动画都会调用ActivityTransitionCoordinatormoveSharedElementsToOverlay处理动画被遮挡问题)。

步骤6 共享元素传送与通知

当完成步骤5时,我们的MainActivity相关信息已经准备好了,下一步通过会自动ActivityTransitionCoordinator发送到下一个界面SecondActivity。当信息全部到达后回调MainActivity中的onSharedElementsArrived函数,用于告诉界面数据已经发送完了你可以在这个回调做一些额外处理或者不处理,当你处理完成后需要发送信息给SecondActivity让其准备动画。

class MainActivity
 		override fun onSharedElementsArrived(
                sharedElementNames: MutableList<String>,
                sharedElements: MutableList<View>,
                listener: OnSharedElementsReadyListener
            ) 
                //告诉SecondActivity我们做好准备了,你开始处理动画吧
                //这也是父类函数默认处理。super.onSharedElementsArrived(sharedElementNames, sharedElements, listener)
                //如果不进行调用那么界面将假死
                listener.onSharedElementsReady()
                
            

MainActivity调用listener.onSharedElementsReady()后第二个界面也会回调onSharedElementsArrived函数

class SecondActivity
	override fun onSharedElementsArrived(
                sharedElementNames: MutableList<String>?,
                sharedElements: MutableList<View>?,
                listener: OnSharedElementsReadyListener?
            ) 

                super.onSharedElementsArrived(sharedElementNames, sharedElements, listener)

            
            

步骤7

此时所有数据都准备好了,SecondActivity会调用onRejectSharedElements回调相关共享元素中被排除的项

class SecondActivity

  override fun onRejectSharedElements(rejectedSharedElements: MutableList<View>?) 
                super.onRejectSharedElements(rejectedSharedElements)
            

步骤8 用上界面传入的快照信息创建View

这里会回调SecondActivityonCreateSnapshotView传入的参数就是上个MainActivity中的onCaptureSharedElementSnapshot传出参数中。

class SecondActivity

  override fun onCreateSnapshotView(context: Context?, snapshot: Parcelable?): View
          return super.onCreateSnapshotView(context, parcelable)
	

                

这里需要返回一个SnapshotView,然后模拟其各种测量和布局行为,然后将这个SnapshotView布局后的各种信息写入SecondActivity 对应transitionNameView中。然后这些属性会被Transition#captureStartValues捕获。在captureStartValues捕获后还原状态给captureEndValues

这里设计主要是为了完善初始化状态和结束状态的捕获。

步骤9

回调SecondActivityonSharedElementStart函数标志准备开始构建共享元素的信息。
你可以onSharedElementStart再次调整中再次修改布局参数以修改起始动画参数(captureStartValues会被影响),其中你要注意sharedElements中的view是SecondActivity,并且其坐标系已经被修正到相对sharedElementSnapshots中对应View的坐标。
比如:
sharedElements有一个View是一个头像ImageView,在MainActivity屏幕坐标为 (100,100),而在SecondActivity屏幕坐标为(200,200),那么这个View会被移动到100,100),

class SecondActivity

 //这里的View信息
  override fun onSharedElementStart(
                sharedElementNames: MutableList<String>?,
                sharedElements: MutableList<View>?,
                sharedElementSnapshots: MutableList<View>?
            ) 
                super.onSharedElementStart(
                    sharedElementNames,
                    sharedElements,
                    sharedElementSnapshots
                )
                log("onSharedElementStart")
            
            

步骤10

收集共享动画的开始状态信息。这初始状态其实就是前面创建SnapshotView的坐标等信息复制到SecondActivity对应的View。这将回调Transition#captureStartValues

步骤11

收集完开始始化信息 回调onSharedElementEnd此时你可以再次修改布局以影响Transition#captureEndValues收集结果。注意这里的sharedElements相关视图位置会被修正回来(onSharedElementStart中坐标被修改了)。

比如:
sharedElements有一个View是一个头像ImageView,在MainActivity屏幕坐标为 (100,100),而在SecondActivity屏幕坐标为(200,200),那么这个View会在onSharedElementStart时被移动到100,100),而在onSharedElementEnd回调中这个坐标会被修正回(200,200)

class SecondActivity



	override fun onSharedElementEnd(
                sharedElementNames: MutableList<String>?,
                sharedElements: MutableList<View>?,
                sharedElementSnapshots: MutableList<View>?
            ) 
                super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots)
               

            
            

步骤12

回调Transition#captureEndValues收集信息

步骤13

回调Transition#createAnimator回调动画

FAQ

(1) window.allowEnterTransitionOverlap 在实际测试中并没有看到开关后的明显区别

(2) 共享元素动画设置必须在 setContentView()后,如windwo.sharedElementExitTransition。否则进出场的动画会被主题覆盖。

(3) Transition是否调用Transition#createAnimatorTransition#getTransitionProperties返回的存储TransitionValues中的values变化判断。简单来说如下代码是不会调用Transition#createAnimator

//本例中values中开始和结束的MyKey数值虽然不一样,但是依然不会调用`Transition#createAnimator`
 override fun captureEndValues(transitionValues: android.transition.TransitionValues) 
            super.captureEndValues(transitionValues)
                transitionValues?.values?.put( "MyKey", "1")
            
        

        override fun captureStartValues(transitionValues: android.transition.TransitionValues?) 
            super.captureStartValues(transitionValues)
                transitionValues?.values?.put( "MyKey",  "2" )
            

        

具体原因可参阅如下源码代码判断

class Transition
  public boolean isTransitionRequired( TransitionValues startValues, TransitionValues endValues) 
        boolean valuesChanged = false;
        // if startValues null, then transition didn't care to stash values,
        // and won't get canceled
        if (startValues != null && endValues != null) 
        	//注意看着只有指定的属性变化才能视为view变化执行动画
            String[] properties = getTransitionProperties();
            if (properties != null) 
                int count = properties.length;
                for (int i = 0; i < count; i++) 
                    if (isValueChanged(startValues, endValues, properties[i])) 
                        valuesChanged = true;
                        break;
                    
                
             else 
                for (String key : startValues.values.keySet()) 
                    if (isValueChanged(startValues, endValues, key)) 
                        valuesChanged = true;
                        break;
                    
                
            
        
        return valuesChanged;
    

正确写法

//本例中values中开始和结束的MyKey数值虽然不一样,但是依然不会调用`Transition#createAnimator`
 override fun captureEndValues(transitionValues: android.transition.TransitionValues) 
            super.captureEndValues(transitionValues)
                transitionValues?.values?.put( "MyKey", "1")
            
        

        override fun captureStartValues(transitionValues: android.transition.TransitionValues?) 
            super.captureStartValues(transitionValues)
                transitionValues?.values?.put( "MyKey",  "2" )
            

        
        //告诉Transition如果MyKey变化就要执行动画
           override fun getTransitionProperties(): Array<String> 
            return super.getTransitionProperties() + "MyKey"
        

(4) Transition动画时间由Transition.duration决定而非返回的createAnimator返回的动画时间

(5) 关闭或者自定义转场动画

以上是关于Android 转场动画源码剖析的主要内容,如果未能解决你的问题,请参考以下文章

Android 转场动画源码剖析

Android中的转场动画以及material-components-android 使用

Android中的转场动画以及material-components-android 使用

android转场动画windowAnimation和ActivityAnimation的区别

android转场动画windowAnimation和ActivityAnimation的区别

android转场动画windowAnimation和ActivityAnimation的区别