最近在我的应用KeepassA中碰到了一个诡异的过渡动画问题

API版本:29

正常状态应该如下:

android 属性动画失效,日常爬坑-Android Transitions动画失效-编程之家

当我从一级设置界面,进入二级设置界面后,并从二级设置界面返回时,一级界面当回主页的过渡动画消失了!!

android 属性动画失效,日常爬坑-Android Transitions动画失效-编程之家

原因分析

阅读源码发现,返回时调用的finishAfterTransition()最终会调用ActivityTransitionState的startExitBackTransition方法,但是当我从二级界面返回到一级界面,并从一级界面返回主页时,pendingExitNames变为了空,导致直接走了finish,而没有走过渡动画的逻辑。

public void finishAfterTransition(){

if (!mActivityTransitionState.startExitBackTransition(this)) {

finish();

}

}

public boolean startExitBackTransition(final Activity activity){

ArrayList pendingExitNames = getPendingExitNames();

if (pendingExitNames == null || mCalledExitCoordinator != null) {

return false;

} else {

}

….

}

为什么会出现pendingExitNames为空的情况呢,继续阅读源代码,通过Activity的生命周期可以知道,每当activity开始活动时(从二级界面返回一级界面会回调onStart),导致重新调用了onNewActivityOptions方法。

/** @hide */

public void onNewActivityOptions(ActivityOptions options){

mActivityTransitionState.setEnterActivityOptions(this, options); // 重新设置了option

if (!mStopped) {

mActivityTransitionState.enterReady(this);

}

}

在mActivityTransitionState.setEnterActivityOptions(this, options);中会重新设置共享元素,

如果当前activity已经停止(启动了二级页面,并从二级界面返回)则会调用mActivityTransitionState.enterReady重新构建过渡动画。

public void enterReady(Activity activity){

// 重新创建过渡场景,但是该场景的共享元素列表 sharedElementNames 没有数据

mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,

resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),

mEnterActivityOptions.isCrossTask());

if (!mIsEnterPostponed) {

startEnter();

}

}

这个时候,只是共享元素的列表大小为0,并没有为null,还达不到那个条件,继续阅读代码,看到EnterTransitionCoordinator找到了一个处理共享元素状态的方法onReceiveResult,在这里面看到,只有接收到的消息类型为MSG_ALLOW_RETURN_TRANSITION才会给mPendingExitNames赋值。

也就意味着自由接受到MSG_ALLOW_RETURN_TRANSITION消息,Activity才会执行退出动画

但是这个消息接收又是从那个地方回调的呢?

@Override

protected void onReceiveResult(int resultCode, Bundle resultData){

switch (resultCode) {

….

case MSG_ALLOW_RETURN_TRANSITION:

if (!mIsCanceled) {

mPendingExitNames = mAllSharedElementNames;

}

break;

}

}

查看该消息的说明:

/**

* Sent by Activity#startActivity to notify the entering activity that enter animation for

* back is allowed. If this message is not received, the default exit animation will run when

* backing out of an activity (instead of the 'reverse' shared element transition).

*/

public static final int MSG_ALLOW_RETURN_TRANSITION = 108;

意思是只有Activity启动时,ActicityThread 才会发送该消息

那么这个消息是在哪个地方发送的呢,阅读源码发现其是在ExitTransitionCoordinator中发送的。

protected void notifyComplete(){

if (isReadyToNotify()) {

if (!mSharedElementNotified) {

mSharedElementNotified = true;

delayCancel();

if (!mActivity.isTopOfTask()) {

// 在这发送

mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null);

}

if (mListener == null) {

mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);

notifyExitComplete();

} else {

final ResultReceiver resultReceiver = mResultReceiver;

final Bundle sharedElementBundle = mSharedElementBundle;

mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,

new OnSharedElementsReadyListener() {

@Override

public void onSharedElementsReady(){

resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,

sharedElementBundle);

notifyExitComplete();

}

});

}

} else {

notifyExitComplete();

}

}

}

那么notifyComplete是在什么时候被调用呢?继续翻看源码。

源码很复杂,需要分为两部分,我总结了两个图来说明整体的流程:

A -> B

android 属性动画失效,日常爬坑-Android Transitions动画失效-编程之家

B -> A

android 属性动画失效,日常爬坑-Android Transitions动画失效-编程之家

大体流程就是:

A 启动 B, A 执行完成退出动画后,会发送MSG_ALLOW_RETURN_TRANSITION给B,当B退出时就可以执行共享元素动画,同时A会移除自己的退出动画

B 返回A,A会重新创建EnterTransitionCoordinator,这没毛病,问题就出在,已经没有人会发送MSG_ALLOW_RETURN_TRANSITION给A,导致EnterTransitionCoordinator.mPendingExitNames这个对象无法被初始化。

当A退出时,会导致mPendingExitNames为null

private ArrayList getPendingExitNames(){

if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {

mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();

}

return mPendingExitNames;

}

也就意味着,无法执行退出动画。

public boolean startExitBackTransition(final Activity activity){

ArrayList pendingExitNames = getPendingExitNames();

if (pendingExitNames == null || mCalledExitCoordinator != null) {

return false;

} else {

if (!mHasExited) {

mHasExited = true;

Transition enterViewsTransition = null;

ViewGroup decor = null;

boolean delayExitBack = false;

if (mEnterTransitionCoordinator != null) {

enterViewsTransition = mEnterTransitionCoordinator.getEnterViewsTransition();

decor = mEnterTransitionCoordinator.getDecor();

delayExitBack = mEnterTransitionCoordinator.cancelEnter();

mEnterTransitionCoordinator = null;

if (enterViewsTransition != null && decor != null) {

enterViewsTransition.pause(decor);

}

}

mReturnExitCoordinator = new ExitTransitionCoordinator(activity,

activity.getWindow(), activity.mEnterTransitionListener, pendingExitNames,

null, null, true);

if (enterViewsTransition != null && decor != null) {

enterViewsTransition.resume(decor);

}

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吗??

解决

1、对于多级页面跳转,不使用共享元素动画,使用默认的进场动画(推荐)

overridePendingTransition(R.anim.translate_right_in, R.anim.translate_left_out)

2、在恢复Activity的时候,重新设置共享元素(不推荐,需要使反射系统隐藏Api,将来可能会失效)

不建议使用这个,因为会面临着机型兼容的问题,以及在API 29 以上需要使用第三方库FreeReflection来实现隐藏API的反射。

根据前面的分析,共享元素动画消失,是因为A在启动B的时候,在A的退出动画执行完成后,会将自身的退出动画移除,并且会重新创建EnterTransitionCoordinator,但是并没有给EnterTransitionCoordinator.mPendingExitNames`赋值。

因此,可以使用ActivityOptions.makeSceneTransitionAnimation(Activity, Pari)重新创建退出动画,并且通过通过反射,给ActivityTransitionState.mPendingExitNames设置共享元素。

构建共享元素

/**

* @param sharedElements 共享元素属性

*/

open fun buildSharedElements(vararg sharedElements: Pair): ArrayList {

val names = ArrayList()

for (i in sharedElements.indices) {

val sharedElement: Pair = sharedElements[i]

val sharedElementName = sharedElement.second

?: throw IllegalArgumentException("Shared element name must not be null")

names.add(sharedElementName)

val view = sharedElement.first

?: throw IllegalArgumentException("Shared element must not be null")

views.add(sharedElement.first)

}

return names

}

使用反射,给mPendingExitNames赋值

@SuppressLint("PrivateApi") fun updateResume(activity: Activity) {

try {

ActivityOptions.makeSceneTransitionAnimation(this)

val stateField: Field = ReflectionUtil.getField(

Activity::class.java,

"mActivityTransitionState"

)

val stateObj = stateField.get(activity)

val activityTransitionStateClazz =

classLoader.loadClass("android.app.ActivityTransitionState")

val mPendingExitNamesField: Field = ReflectionUtil.getField(

activityTransitionStateClazz,

"mPendingExitNames"

)

// 设置当前Activity需要的共享元素

val appIcon =

Pair(binding.appIcon, getString(string.transition_app_icon))

val dbName =

Pair(binding.dbName, getString(string.transition_db_name))

val dbVersion =

Pair(binding.dbVersion, getString(string.transition_db_version))

val dbLittle =

Pair(binding.arrow, getString(string.transition_db_little))

mPendingExitNamesField.set(stateObj, buildSharedElements(appIcon, dbName, dbVersion, dbLittle))

} catch (e: java.lang.Exception) {

e.printStackTrace()

}

}

最终的效果:

android 属性动画失效,日常爬坑-Android Transitions动画失效-编程之家