美文网首页
Activity Transition动画和共享元素动画失效问题

Activity Transition动画和共享元素动画失效问题

作者: 水月沐風 | 来源:发表于2024-11-06 14:35 被阅读0次

概述

近期项目中有涉及到动画相关的内容,重新将尘封已久的 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

解决思路

  1. 亮屏后强制对CardListActivity执行一次 ActivityTransitionState#onResume,然而通过源码分析发现其只针对入场动画,而本场景下其实是出场动画下的过渡动画View状态失效了。

  2. 通过反射先将 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等方式协助我们解决一些不太好处理的问题,至少还能追溯到问题产生的原因,而不只是面对问题望洋兴叹罢了。

相关文章

网友评论

      本文标题:Activity Transition动画和共享元素动画失效问题

      本文链接:https://www.haomeiwen.com/subject/djltjjtx.html