概述
近期项目中有涉及到动画相关的内容,重新将尘封已久的 Activity 动画拾起来了。由于场景的复杂程度相较于多年前无法同日而语,就导致遇到了各种稀奇古怪的问题,本文将记录一下在 Activity 透明背景加上息屏亮屏等复杂场景下 Transition 导致的 View 显示状态问题。
问题描述
这里为了图省事,就不放现象视频了,直接通过示意图简单描述下问题现象。
现象示意图再简单描述下问题现象:CardListActivity 是用于承载一个卡片列表的页面,而 CardDetailsActivity 则是用于显示卡片的详细信息,主要通过Android原生的过渡动画Transition和ShareElementTransition共享元素动画来实现两个页面的跳转。
然而,由于设计要求,当从列表页跳转到详情页时,需要隐藏列表页的视图,同时将 CardDetailsActivity 设置为透明背景。最初一切正常,但停留在详情页时用户手机锁屏并亮屏后,底层的列表页内容又透出来了,并没有隐藏,于是出现了上图中的CardListActivity和CardDetailsActivity内容互相叠加的异常现象。
原因分析
其实不止是锁屏亮屏场景,元素共享动画和Transition动画在众多复杂场景下都会存在一些View状态问题,例如多级页面下返回共享动画丢失、按home键再返回后也会出现上述类似现象。下面就主要针对锁屏亮屏的场景来分析一下原因。
首先来看下列表页CardListActivity和详情页CardDetailsActivity息屏、亮屏后的生命周期变化:
息屏 | 亮屏 | |
---|---|---|
CardListActivity | onStop | onRestart->onStart |
CardDetailsActivity | onStop | onRestart->onStart->onResume |
猜测可能是 Activity#onStop 中做了一些重置操作,导致view状态被恢复。为了验证猜想,看一下 ActivityTransitionState#onStop 做了些什么事(Activity#onStop中会调用此方法):
public void onStop(Activity activity) {
restoreExitedViews();
if (mEnterTransitionCoordinator != null) {
getPendingExitNames(); // Set mPendingExitNames before clearing
mEnterTransitionCoordinator.stop();
mEnterTransitionCoordinator = null;
}
if (mReturnExitCoordinator != null) {
mReturnExitCoordinator.stop(activity);
mReturnExitCoordinator = null;
}
}
private void restoreExitedViews() {
if (mCalledExitCoordinator != null) {
mCalledExitCoordinator.resetViews();
mCalledExitCoordinator = null;
}
}
public void resetViews() {
ViewGroup decorView = getDecor();
if (decorView != null) {
TransitionManager.endTransitions(decorView);
}
if (mTransitioningViews != null) {
showViews(mTransitioningViews, true);
setTransitioningViewsVisiblity(View.VISIBLE, true);
}
showViews(mSharedElements, true);
mIsHidden = true;
if (!mIsReturning && decorView != null) {
decorView.suppressLayout(false);
}
moveSharedElementsFromOverlay();
clearState();
}
protected void setTransitioningViewsVisiblity(int visiblity, boolean invalidate) {
final int numElements = mTransitioningViews == null ? 0 : mTransitioningViews.size();
for (int i = 0; i < numElements; i++) {
final View view = mTransitioningViews.get(i);
if (invalidate) {
// Allow the view to be invalidated by the visibility change
view.setVisibility(visiblity);
} else {
// Don't invalidate the view with the visibility change
view.setTransitionVisibility(visiblity);
}
}
}
protected void showViews(ArrayList<View> views, boolean setTransitionAlpha) {
int count = views.size();
for (int i = 0; i < count; i++) {
showView(views.get(i), setTransitionAlpha);
}
}
private void showView(View view, boolean setTransitionAlpha) {
Float alpha = mOriginalAlphas.remove(view);
if (alpha != null) {
view.setAlpha(alpha);
}
if (setTransitionAlpha) {
view.setTransitionAlpha(1f);
}
}
果然,ActivityTransitionState#onStop
中会执行 restoreExitedViews
方法,当 mCalledExitCoordinator
不为空时会重置View状态,即此场景下页面的View元素状态被改写为从不可见到可见了,也就出现了开头我们看到的现象。
其他类似共享元素动画返回失效问题参考:https://juejin.cn/post/6844904182592307207
解决思路
-
亮屏后强制对CardListActivity执行一次
ActivityTransitionState#onResume
,然而通过源码分析发现其只针对入场动画,而本场景下其实是出场动画下的过渡动画View状态失效了。 -
通过反射先将
ActivityTransitionState#mCalledExitCoordinator
设置为null,防止其在 onStop 生命周期下过渡动画View显示状态被重置,然后在重新亮屏后恢复到之前的值,保证详情页返回到列表页后过渡动画View的状态正常。
下面主要对方案二进行实操,核心代码如下:
CardListActivity:
override fun onCreate(savedInstanceState: Bundle?) {
with(window) {
requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
enterTransition = null
exitTransition = Fade().apply {
excludeTarget(R.id.background_container, true)
}
}
}
internal class ScreenStatusReceiver(activity: CardListActivity?) : SafeBroadcastReceiver<CardListActivity?>(activity) {
override fun onReceive(context: Context, intent: Intent) {
if (mWeakTarget.get() == null || mWeakTarget.get()!!.isStarted) {
return
}
if (Intent.ACTION_SCREEN_OFF == intent.action) {
Log.i(TAG, "onReceive, screen off")
mWeakTarget.get()!!.mIsScreenOff = true
} else if (Intent.ACTION_SCREEN_ON == intent.action) {
Log.i(TAG, "onReceive, screen on")
mWeakTarget.get()!!.mIsScreenOff = false
mWeakTarget.get()!!.recoverExitTransitionCoordinator()
// mWeakTarget.get()!!.makeActivityTransitionStateEnterReady()
}
}
}
private var mExitTransitionCoordinator: Any? = null
private var mIsScreenOff = false
/**
* 此处防止ActivityTransitionState在onStop将transitionViews恢复到初始状态
*/
private fun resetExitTransitionCoordinator() {
val activityTransitionStateField = ReflectUtils.getField(
ReflectUtils.getClass("android.app.Activity"),
"mActivityTransitionState",
ReflectUtils.getClass("android.app.ActivityTransitionState")
)
val activityTransitionState = activityTransitionStateField?.get(this)
val exitTransitionCoordinatorField = ReflectUtils.getField(
ReflectUtils.getClass("android.app.ActivityTransitionState"),
"mCalledExitCoordinator",
ReflectUtils.getClass("android.app.ExitTransitionCoordinator")
)
Log.i(TAG, "resetExitTransitionCoordinator, activityTransitionState: $activityTransitionState")
if (exitTransitionCoordinatorField != null && activityTransitionState != null) {
val exitTransitionCoordinator = exitTransitionCoordinatorField.get(activityTransitionState)
Log.i(TAG, "resetExitTransitionCoordinator, set mCalledExitCoordinator to null, origin: $exitTransitionCoordinator")
if (exitTransitionCoordinator != null) {
mExitTransitionCoordinator = exitTransitionCoordinator
}
exitTransitionCoordinatorField.set(activityTransitionState, null)
}
}
/**
* 在特定时机需要恢复transitionViews的状态,防止返回后view不可见
* 1.亮屏时恢复
* 2.在onResume中兜底
*/
private fun recoverExitTransitionCoordinator() {
val activityTransitionStateField = ReflectUtils.getField(
ReflectUtils.getClass("android.app.Activity"),
"mActivityTransitionState",
ReflectUtils.getClass("android.app.ActivityTransitionState")
)
val activityTransitionState = activityTransitionStateField?.get(this)
val exitTransitionCoordinatorField = ReflectUtils.getField(
ReflectUtils.getClass("android.app.ActivityTransitionState"),
"mCalledExitCoordinator",
ReflectUtils.getClass("android.app.ExitTransitionCoordinator")
)
Log.i(TAG, "recoverExitTransitionCoordinator, activityTransitionState: $activityTransitionState, mExitTransitionCoordinator: $mExitTransitionCoordinator")
if (exitTransitionCoordinatorField != null && activityTransitionState != null && mExitTransitionCoordinator != null) {
Log.d(TAG, "recoverExitTransitionCoordinator, reset to old mExitTransitionCoordinator")
exitTransitionCoordinatorField.set(activityTransitionState, mExitTransitionCoordinator)
}
}
private fun makeActivityTransitionStateEnterReady() {
val activityTransitionStateClz = ReflectUtils.getClass("android.app.ActivityTransitionState")
val activityTransitionStateField = ReflectUtils.getField(
ReflectUtils.getClass("android.app.Activity"),
"mActivityTransitionState",
activityTransitionStateClz
)
val activityTransitionState = activityTransitionStateField?.get(this)
val enterReadyMethod = activityTransitionStateClz.getDeclaredMethod("enterReady", Activity::class.java)
enterReadyMethod.isAccessible = true
enterReadyMethod.invoke(activityTransitionState, this)
}
private fun registerScreenStatusReceiver() {
val screenStatusIF = IntentFilter()
screenStatusIF.addAction(Intent.ACTION_SCREEN_OFF)
screenStatusIF.addAction(Intent.ACTION_SCREEN_ON)
registerReceiver(mScreenStatusReceiver, screenStatusIF)
}
override fun onStart() {
super.onStart()
isStarted = true
}
override fun onStop() {
isStarted = false
resetExitTransitionCoordinator()
super.onStop()
}
override fun onResume() {
if (mIsScreenOff) {
recoverExitTransitionCoordinator()
}
super.onResume()
}
另外还加入了兜底逻辑,防止快速息屏亮屏后状态丢失问题,导致View无法恢复到显示状态。需要特别说明的是,该方案只是一个思路或者说是临时过渡方案,不建议在稳定版本中使用,因为尚未经过验证,同时Android高版本上的反射限制也是一个问题。
总结
本文只是记录一下解决问题的思路,例如通过Layout inspector或debug等方式协助我们解决一些不太好处理的问题,至少还能追溯到问题产生的原因,而不只是面对问题望洋兴叹罢了。
网友评论