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 删减贡献元素的数量
在过滤需要真正执行动画元素后开始调用退出动画的Transition
的captureStartValues
和captureEndValues
函数用于捕获需要执行退出动画的元素。
//为退出动画执行收集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 共享元素动画信息传递
MainActivity
和SecondActivity
都需要等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
前捕获的。具体ActivityTransitionCoordinator
在captureSharedElementState
函数中
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
这里会回调SecondActivity
的onCreateSnapshotView
传入的参数就是上个MainActivity
中的onCaptureSharedElementSnapshot
传出参数中。
class SecondActivity
override fun onCreateSnapshotView(context: Context?, snapshot: Parcelable?): View
return super.onCreateSnapshotView(context, parcelable)
这里需要返回一个SnapshotView
,然后模拟其各种测量和布局行为,然后将这个SnapshotView
布局后的各种信息写入SecondActivity
对应transitionName
的View
中。然后这些属性会被Transition#captureStartValues
捕获。在captureStartValues
捕获后还原状态给captureEndValues
。
这里设计主要是为了完善初始化状态和结束状态的捕获。
步骤9
回调SecondActivity
的onSharedElementStart
函数标志准备开始构建共享元素的信息。
你可以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#createAnimator
由Transition#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中的转场动画以及material-components-android 使用
Android中的转场动画以及material-components-android 使用
android转场动画windowAnimation和ActivityAnimation的区别