透明 Activity 及生命周期探索
在多 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
设置为非透明,或者再启动一个非透明主题的CActivity
,AActivity
会依次回调onResume
,onPause
和onStop
,而在 Android 11 及以上则只会回调onStop
,所以在使用上述的方式时,要特别注意生命周期异常回调是否会影响业务正常表现。
总结,优先使用 xml 的方式设置透明主题,当通过 xml 设置主题不符合需求的时候,可以通过 API+反射方式设置,但是需要注意在低版本上的生命周期。