iKirby's Blog/

Android 渐变切换应用主题

我自己写的 IT之家 App 中使用的主题切换方式是直接重新创建 Activity ,但是这样就会有一个问题,切换主题时没有过渡效果,而且在 Android 9 上会闪一下。因为要重新创建 Activity ,就没办法在上面覆盖一个 View 做渐变动画;而一个一个给 View 使用动画又太麻烦了。然后我想到了通过另一个 Activity 的退出效果做动画的方案。不知道这个方案算不算好,不过至少达到了我预期的效果,而且实现起来也不复杂。


这个方案的思路是先获取当前 Activity 的屏幕截图,然后在调用 recreate() 之前无动画启动过渡用的 Activity 并显示这张截图,最后以渐变的动画退出过渡 Activity 。

先实现当前 Activity 截图的获取。

val rootView = window.decorView.rootView
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 在 Android O 和更高版本使用 PixelCopy 获取截图
    // RGB_565 的视觉效果也可以,但是在 Android 9 上会导致 PixelCopy 失败
    val bitmap = Bitmap.createBitmap(rootView.width, rootView.height, Bitmap.Config.ARGB_8888)
    val locationOfRootView = IntArray(2)
    rootView.getLocationInWindow(locationOfRootView)
    PixelCopy.request(
        window,
        Rect( // 截取的范围
            locationOfRootView[0],
            locationOfRootView[1],
            locationOfRootView[0] + rootView.width,
            locationOfRootView[1] + rootView.height
        ),
        bitmap,
        { copyResult ->
            if (copyResult == PixelCopy.SUCCESS) { // 当 PixelCopy 结果成功
                // 由于 Intent 传递限制数据量,暂时想到用静态变量
                ThemeSwitchTransitionActivity.screenshot = bitmap
                startThemeSwitchTransition()
            }
        },
        Handler()
    )
} else { // 在低版本系统中使用缓存获取截图
    rootView.isDrawingCacheEnabled = true
    ThemeSwitchTransitionActivity.screenshot = Bitmap.createBitmap(rootView.drawingCache)
    rootView.isDrawingCacheEnabled = false
    startThemeSwitchTransition()
}

startThemeSwitchTransition() 方法中,重点是将当前 Activity 的状态栏和导航栏的样式 systemUiVisibility 进行传递,直接在过渡用 Activity 中设置,这样在过渡用的 Activity 中就不需要再判断当前应用使用的是什么主题来设置对应的样式。然后只需要在启动 Activity 时使用 overridePendingTransition() 设置为无动画效果就可以了。

private fun startThemeSwitchTransition() {
    val intent = Intent(this, ThemeSwitchTransitionActivity::class.java).apply {
        putExtra(KEY_SYSTEM_UI_VISIBILITY, window.decorView.systemUiVisibility)
    }
    startActivity(intent)
    overridePendingTransition(0, 0)
    Handler().postDelayed({ // 设置一点延迟,避免闪烁
        recreate()
    }, 50)
}

接下来就是 ThemeSwitchTransitionActivity 的内容了,这里说几个需要注意的地方,完整代码可以看文末的链接。

使用 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 来使 Window 扩展到整个屏幕,避免状态栏和导航栏占用空间。

window.setFlags(
    WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
    WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
)

还原之前的 systemUiVisibility

window.decorView.systemUiVisibility = intent.getIntExtra(KEY_SYSTEM_UI_VISIBILITY, 0)

在结束 Activity 时,设置渐变的退出动画(我直接使用了系统自带的淡出动画)。

Handler().postDelayed({ // 设置一点延迟避免闪烁,实际测试不影响体验
    finish()
    overridePendingTransition(0, android.R.anim.fade_out)
}, 100)

避免用户按下返回键影响动画效果。

override fun onBackPressed() {
}

避免屏幕旋转影响效果,在 Manifest 中为这个 Activity 加上属性 android:screenOrientation="locked"

<activity
    android:name=".ui.activity.ThemeSwitchTransitionActivity"
    android:screenOrientation="locked"
    android:theme="@style/Theme.MaterialComponents.NoActionBar" />

另外,没必要使用单独的布局 XML ,只需要一个 ImageView 即可。

val imageView = ImageView(this)
imageView.layoutParams =
    ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
setContentView(imageView)

最终效果演示如下(MP4 ,大小 217.8KB),完整代码可以见 MainActivity.ktThemeSwitchTransitionActivity.kt

留下一条评论

仅有 1 条评论

  1. YvesCheung:

    RGB_565 的视觉效果也可以,但是在 Android 9 上会导致 PixelCopy 失败。
    遇到同样的问题困扰很久,搜了全网终于在你这里找到答案。

    2022-10-21 11:55 回复