透明 Activity 及生命周期探索

1059

在多 Activity 架构的程序中,透明 Acitvity 的需求比较常见,比如想要实现侧滑返回或下拉退出页面功能,而 Activity 默认会有一个黑色背景,去除黑色背景的常见做法是指定 Activity 的主题为透明主题:

<style name="Theme.ActivityLifecycle.Transparent" parent="Theme.ActivityLifecycle">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
</style>

当 Activity 主题为透明时,若启动此 Activity,其下面的 Activity 只会回调onPause生命周期,而不会回调onStop,这其实也符合对应生命周期的定义:当生命周期变为STARTED时代表 Activity 可见,RESUMED时代表 Activity 可交互,当我们将 Activity 设置为透明时,系统自然认为其下面的 Activity 可见,故只会回调onPause;当透明 Activity 被移除时,下面的 Activity 回调onResume

但有些情况下,我们不希望修改 Activity 的主题,可能出于直接修改主题上线难以回退的原因,也可能是因为想让其下面的 Activity 继续回调onStop,所以我们希望能做到运行时修改 Activity 的主题,但不管是我们尝试在super.onCreate之前还是之后调用setTheme,亦或者通过其他方式都无法做到修改为透明主题,似乎系统自动忽略了android:windowIsTranslucent属性。

只有透明的属性会失效,其他主题的设置会正常生效

既然设置主题的方式无法成功,自然需要寻找其他方式。在 Android 11 (Android Q, API 30) 及以上,Activity 新开放了一个新的 APIsetTranslucent可以设置 Activity 为是否透明,其内部实现仍旧是调用了两个接口,因此在 11 及以上我们可以通过此方法,而 11 以下则通过反射实现:

object ActivityTranslucentUtil {

    fun convertActivityToTranslucent(activity: Activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            activity.setTranslucent(true)
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            convertActivityToTranslucentAfterL(activity)
        } else {
            convertActivityToTranslucentBeforeL(activity)
        }
        activity.window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
    }

    private fun convertActivityToTranslucentBeforeL(activity: Activity?) {
        try {
            val classes: Array<Class<*>> = Activity::class.java.declaredClasses
            var translucentConversionListenerClazz: Class<*>? = null
            for (clazz in classes) {
                if (clazz.simpleName.contains("TranslucentConversionListener")) {
                    translucentConversionListenerClazz = clazz
                }
            }
            val method: Method? = Activity::class.java.getDeclaredMethod(
                "convertToTranslucent",
                translucentConversionListenerClazz
            )
            method?.isAccessible = true
            method?.invoke(activity, arrayOf<Any?>(null))
        } catch (_: Throwable) {
        }
    }

    private fun convertActivityToTranslucentAfterL(activity: Activity) {
        try {
            val getActivityOptions: Method? = Activity::class.java.getDeclaredMethod("getActivityOptions")
            getActivityOptions?.isAccessible = true
            val options: Any? = getActivityOptions?.invoke(activity)
            val classes: Array<Class<*>> = Activity::class.java.declaredClasses
            var translucentConversionListenerClazz: Class<*>? = null
            for (clazz in classes) {
                if (clazz.simpleName.contains("TranslucentConversionListener")) {
                    translucentConversionListenerClazz = clazz
                }
            }
            val convertToTranslucent: Method? = Activity::class.java.getDeclaredMethod(
                "convertToTranslucent",
                translucentConversionListenerClazz, ActivityOptions::class.java
            )
            convertToTranslucent?.isAccessible = true
            convertToTranslucent?.invoke(activity, null, options)
        } catch (_: Throwable) {
        }
    }

    fun convertActivityFromTranslucent(activity: Activity?) {
        activity ?: return
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                activity.setTranslucent(false)
            } else {
                val method = Activity::class.java.getDeclaredMethod("convertFromTranslucent")
                method.isAccessible = true
                method.invoke(activity)
            }
        } catch (_: Throwable) {
        }
    }
}

上面的代码从表现上来看符合我们的需求,但有一些生命周期的问题需要注意。

在** Android 10 及以下**,当在AActivity上面放一个BActivity时,若我们调用上面的代码将BActivity设置为透明,AActivity会立刻回调onStart,其生命周期变为STARTED,整体回调如下:

// 启动 AActivity
AActivity#onCreate
AActivity#onStart
AActivity#onResume
// 启动 BActivity
AActivity#onPause
AActivity#onStop
// 设置 BActivity 为透明
AActivity#onStart

此时生命周期回调也符合我们的预期。但此时如果我们再将BActivity设置为非透明,或者再启动一个非透明主题CActivityAActivity会依次回调onResumeonPauseonStop,而在 Android 11 及以上则只会回调onStop,所以在使用上述的方式时,要特别注意生命周期异常回调是否会影响业务正常表现。

总结,优先使用 xml 的方式设置透明主题,当通过 xml 设置主题不符合需求的时候,可以通过 API+反射方式设置,但是需要注意在低版本上的生命周期。