在安卓悬浮窗中使用Compose
在安卓开发中,实现悬浮窗的功能是非常常见的。但如果我们想要在悬浮窗中使用Jetpack Compose进行UI组件的构建,则会遇到一些挑战。本文详细介绍了如何解决这些问题,包括创建一个自定义的LifecycleOwner、设置ViewTreeLifecycleOwner以及实现ViewTreeSavedStateRegistryOwner等步骤。

安卓中实现悬浮窗的方式有很多,最常见的一种就是通过WindowManager的addView方法,这里可以参考这篇文章

        windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        windowManager?.addView(view, layoutParams)

那么,能在这个悬浮窗中使用Compose吗?

众所周知Compose和View系统还是很好结合的,只要把ComposeView添加到View树上,即

    private fun createView(context: Context): View {
        val view = ComposeView(context).also {
            it.layoutParams = LayoutParams(300.dp, 200.dp)
            it.setContent {
                Text("Hello World!")
            }
        }
        return FrameLayout(context).also {
            it.addView(view)
        }
    }

当然事情没有这么顺利

2025-02-09 16:35:56.080 16874-16874 AndroidRuntime          top.ntutn.floatcompose               E  FATAL EXCEPTION: main
                                                                                                    Process: top.ntutn.floatcompose, PID: 16874
                                                                                                    java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from android.widget.FrameLayout{5b7aba6 VFE...C.. ......I. 0,0-0,0}
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer(WindowRecomposer.android.kt:352)
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer$default(WindowRecomposer.android.kt:325)
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposerFactory$Companion.LifecycleAware$lambda$0(WindowRecomposer.android.kt:168)
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposerFactory$Companion.$r8$lambda$FWAPLXs0qWMqekhMr83xkKattCY(Unknown Source:0)
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposerFactory$Companion$$ExternalSyntheticLambda0.createRecomposer(D8$$SyntheticClass:0)
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposerPolicy.createAndInstallWindowRecomposer$ui_release(WindowRecomposer.android.kt:224)
                                                                                                    	at androidx.compose.ui.platform.WindowRecomposer_androidKt.getWindowRecomposer(WindowRecomposer.android.kt:300)
                                                                                                    	at androidx.compose.ui.platform.AbstractComposeView.resolveParentCompositionContext(ComposeView.android.kt:244)
                                                                                                    	at androidx.compose.ui.platform.AbstractComposeView.ensureCompositionCreated(ComposeView.android.kt:251)
                                                                                                    	at androidx.compose.ui.platform.AbstractComposeView.onAttachedToWindow(ComposeView.android.kt:283)
                                                                                                    	at android.view.View.dispatchAttachedToWindow(Unknown Source:82)
                                                                                                    	at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:7)
                                                                                                    	at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:36)
                                                                                                    	at android.view.ViewRootImpl.performTraversals(Unknown Source:219)
                                                                                                    	at android.view.ViewRootImpl.doTraversal(Unknown Source:36)
                                                                                                    	at android.view.ViewRootImpl$TraversalRunnable.run(Unknown Source:7)
                                                                                                    	at android.view.Choreographer$CallbackRecord.run(Unknown Source:20)
                                                                                                    	at android.view.Choreographer$CallbackRecord.run(Unknown Source:20)
                                                                                                    	at android.view.Choreographer.doCallbacks(Unknown Source:110)
                                                                                                    	at android.view.Choreographer.doFrame(Unknown Source:524)
                                                                                                    	at android.view.Choreographer$FrameDisplayEventReceiver.run(Unknown Source:11)
                                                                                                    	at android.os.Handler.handleCallback(Unknown Source:2)
                                                                                                    	at android.os.Handler.dispatchMessage(Unknown Source:4)
                                                                                                    	at android.os.Looper.loopOnce(Unknown Source:182)
                                                                                                    	at android.os.Looper.loop(Unknown Source:82)
                                                                                                    	at android.app.ActivityThread.main(Unknown Source:123)
                                                                                                    	at java.lang.reflect.Method.invoke(Native Method)
                                                                                                    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(Unknown Source:11)
                                                                                                    	at com.android.internal.os.ZygoteInit.main(Unknown Source:312)

需要一个ViewTreeLifecycleOwner,好说,我们实现一个就行了

    class FloatingWindowLifecycleOwner : LifecycleOwner {
        private val registry = LifecycleRegistry(this)

        override val lifecycle: Lifecycle = registry

        fun onShow() {
            registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
            registry.handleLifecycleEvent(Lifecycle.Event.ON_START)
            registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
        }

        fun onDismiss() {
            registry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
            registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
            registry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        }
    }

然而,即使把这个LifecycleOwner设置到ComposeView的脸上,他仍然跟你说找不到

设置LifecycleOwner

那这找不到肯定是没认真找,所以查看代码,发现Compose只会从 R.id.content 开始找

从contentChild开始查找

R.id.content

那么只能玩阴的了

    private fun createView(context: Context): View {
        val lifecycleOwner = FloatingWindowLifecycleOwner()
        viewTreeLifecycleOwner = lifecycleOwner

        val view = ComposeView(context).also {
            it.layoutParams = LayoutParams(300.dp, 200.dp)
            it.setContent {
                Text("Hello World!")
            }
        }
        return FrameLayout(context).also {
            it.addView(view)
            // dirty hack, ComposeView need this
            it.id = android.R.id.content
            it.setViewTreeLifecycleOwner(lifecycleOwner)
        }
    }

再次运行,然后还是没跑起来

2025-02-09 17:08:15.591 16879-16879 AndroidRuntime          top.ntutn.floatcompose               E  FATAL EXCEPTION: main
                                                                                                    Process: top.ntutn.floatcompose, PID: 16879
                                                                                                    java.lang.IllegalStateException: Composed into the View which doesn't propagateViewTreeSavedStateRegistryOwner!
                                                                                                    	at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1352)
                                                                                                    	at android.view.View.dispatchAttachedToWindow(Unknown Source:82)
                                                                                                    	at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:7)
                                                                                                    	at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:36)
                                                                                                    	at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:36)
                                                                                                    	at android.view.ViewRootImpl.performTraversals(Unknown Source:219)
                                                                                                    	at android.view.ViewRootImpl.doTraversal(Unknown Source:36)
                                                                                                    	at android.view.ViewRootImpl$TraversalRunnable.run(Unknown Source:7)
                                                                                                    	at android.view.Choreographer$CallbackRecord.run(Unknown Source:20)
                                                                                                    	at android.view.Choreographer$CallbackRecord.run(Unknown Source:20)
                                                                                                    	at android.view.Choreographer.doCallbacks(Unknown Source:110)
                                                                                                    	at android.view.Choreographer.doFrame(Unknown Source:524)
                                                                                                    	at android.view.Choreographer$FrameDisplayEventReceiver.run(Unknown Source:11)
                                                                                                    	at android.os.Handler.handleCallback(Unknown Source:2)
                                                                                                    	at android.os.Handler.dispatchMessage(Unknown Source:4)
                                                                                                    	at android.os.Looper.loopOnce(Unknown Source:182)
                                                                                                    	at android.os.Looper.loop(Unknown Source:82)
                                                                                                    	at android.app.ActivityThread.main(Unknown Source:123)
                                                                                                    	at java.lang.reflect.Method.invoke(Native Method)
                                                                                                    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(Unknown Source:11)
                                                                                                    	at com.android.internal.os.ZygoteInit.main(Unknown Source:312)

这回是缺了一个ViewTreeSavedStateRegistryOwner,这个是切换横竖屏等情况恢复状态的,悬浮窗不考虑这些的话,还是照葫芦画瓢搞个空实现。

补齐之后,终于在悬浮窗看到了Compose的组件,可喜可贺。

悬浮窗中使用Compose

最后代码在这里,开工大吉!


最后修改于 2025-02-09