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

API版本:29

正常状态应该如下:

normal

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

android_ta_error

原因分析

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

public void finishAfterTransition() {
if (!mActivityTransitionState.startExitBackTransition(this)) {
finish();
}
}
public boolean startExitBackTransition(final Activity activity) {
ArrayList<String> 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

A -> B

B -> A

B -> A

大体流程就是:

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<String> getPendingExitNames() {
if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {
mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();
}
return mPendingExitNames;
}

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

public boolean startExitBackTransition(final Activity activity) {
ArrayList<String> 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<View, String>)重新创建退出动画,并且通过通过反射,给ActivityTransitionState.mPendingExitNames设置共享元素。

构建共享元素

/**
* @param sharedElements 共享元素属性
*/
open fun buildSharedElements(vararg sharedElements: Pair<View, String>): ArrayList<String> {
val names = ArrayList<String>()
for (i in sharedElements.indices) {
val sharedElement: Pair<View, String> = 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<View, String>(binding.appIcon, getString(string.transition_app_icon))
val dbName =
Pair<View, String>(binding.dbName, getString(string.transition_db_name))
val dbVersion =
Pair<View, String>(binding.dbVersion, getString(string.transition_db_version))
val dbLittle =
Pair<View, String>(binding.arrow, getString(string.transition_db_little))
mPendingExitNamesField.set(stateObj, buildSharedElements(appIcon, dbName, dbVersion, dbLittle))
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}

最终的效果:

最终的效果