前言
= =时隔一年 我竟为了一个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,
android.util.Pair<View, String>(binding.ivAvatar, "ivAvatar"),
android.util.Pair<View, String>(binding.btnTransition, "btnTransition")
)
startActivity(intent, options.toBundle())
我们共享元素的ID其实有一个名字叫transitionName ,这个是一个View属性您可以在代码或者XML赋值。
步骤2
当用户声明MainActivity 中的共享元素后我们边开始收集当前视图的一些信息。并提供一个回调允许开发则再次过滤掉一些不必要的共享元素,执行View信息的收集。
首先被回调onMapSharedElements 函数,你可以在这个函数清楚某个共享元素 ExitSharedElementCallback->SharedElementCallback->onMapSharedElements
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>) {
super.onMapSharedElements(names, sharedElements)
}
在过滤需要真正执行动画元素后开始调用退出动画的Transition 的captureStartValues 和captureEndValues 函数用于捕获需要执行退出动画的元素。
override fun captureEndValues(transitionValues: android.transition.TransitionValues?) {
super.captureEndValues(transitionValues)
}
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(
names: MutableList<String>,
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) {
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;
}
}
}
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
) {
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{
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 的
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 && endValues != null) {
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;
}
}
正确写法
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" )
}
}
override fun getTransitionProperties(): Array<String> {
return super.getTransitionProperties() + "MyKey"
}
(4) Transition 动画时间由Transition.duration 决定而非返回的createAnimator 返回的动画时间
(5) 关闭或者自定义转场动画的之间透明度变化
重新赋值window.enterTransition即可完成自定义,默认为Fade。
参考
使用动画启动 Activity 创建自定义过渡动画
|