在多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+反射方式设置,但是需要注意在低版本上的生命周期。