透明Activity及生命周期探索

1,961 阅读3分钟

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