如何在悬浮窗中使用 Compose?

如何在悬浮窗中使用 Compose?

显示悬浮窗

声明悬浮窗权限。

XML 复制代码
<manifest>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
</manifest>

判断是否拥有显示权限。

Kotlin 复制代码
fun hasOverlayPermission(): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        Settings.canDrawOverlays(weakReference.get())
    } else {
        // Android 23 以下,应用默认是有权限的。
        true
    }
}

打开悬浮窗显示权限设置页

Kotlin 复制代码
fun enterSettings(activity: Activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        activity.startActivity(
            Intent(
                Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                "package:${activity.packageName}".toUri()
            )
        )
    } else {
        // Android 23 以下,应用默认是有权限的,无需申请,也没有设置界面。
    }
}

获取 WindowManager

Kotlin 复制代码
val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager

创建 WindowManager.LayoutParams

Kotlin 复制代码
val layoutParams = WindowManager.LayoutParams().apply {
    format = PixelFormat.RGBA_8888
    flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    gravity = Gravity.START or Gravity.TOP
    width = WindowManager.LayoutParams.MATCH_PARENT
    height = WindowManager.LayoutParams.MATCH_PARENT
    x = 0
    y = 0
    windowAnimations = android.R.style.Animation_Dialog
    // 根据API版本设置窗口类型,优先使用TYPE_APPLICATION_OVERLAY(API 26+),低版本使用TYPE_SYSTEM_ALERT
    type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
    } else {
        WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
    }
}

显示悬浮窗视图

Kotlin 复制代码
fun show() {
    if (isShowing) return
    if (!hasOverlayPermission()) {
        return
    }
    try {
        weakReference.get()?.let { context ->
            windowManager.addView(TextView(context).apply {
                text = "悬浮窗已经打开"
            }, layoutParams)
            isShowing = true
        }
    } catch (e: Exception) {
        isShowing = false
        e.printStackTrace()
    }
}

使用 Compose 显示有什么问题?

直接换用 Compose 显示会报错。

Kotlin 复制代码
windowManager.addView(ComposeView(context).apply {
    setContent {
        Text("悬浮窗已经打开")
    }
}, layoutParams)

会抛出 ViewTreeLifecycleOwner not found from androidx.compose.ui.platform.ComposeView 异常。

自定义 LifecycleOwner 来解决本问题。

MyLifecycleOwner

Kotlin 复制代码
inner class MyLifecycleOwner : SavedStateRegistryOwner, ViewModelStoreOwner {
    private val lifecycleRegistry = LifecycleRegistry(this)
    private val savedStateRegistryController = SavedStateRegistryController.create(this)
    private var view: View? = null

    @Volatile
    private var viewModelStoreInstance: ViewModelStore? = null

    override val lifecycle: Lifecycle
        get() = lifecycleRegistry

    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry

    override val viewModelStore: ViewModelStore
        get() = viewModelStoreInstance ?: synchronized(this) {
            viewModelStoreInstance ?: ViewModelStore().also {
                viewModelStoreInstance = it
            }
        }

    fun performRestore(savedState: Bundle?) {
        savedStateRegistryController.performRestore(savedState)
    }

    fun onCreate() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    fun onStart() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
    }

    fun onResume() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    }

    fun onPause() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    }

    fun onStop() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
    }

    fun onDestroy() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        // 手动清理 ViewModelStore
        viewModelStore.clear()
        view?.apply {
            setViewTreeLifecycleOwner(null)
            setViewTreeViewModelStoreOwner(null)
            setViewTreeSavedStateRegistryOwner(null)
        }
        view = null
    }

    fun attachToView(view: View) {
        // 需要为 Compose 设置 ViewTreeLifecycleOwner
        view.setViewTreeLifecycleOwner(this)
        view.setViewTreeViewModelStoreOwner(this)
        view.setViewTreeSavedStateRegistryOwner(this)
        this.view = view
    }
}

在悬浮窗中使用 Compose

Kotlin 复制代码
var lifecycleOwner = MyLifecycleOwner()

fun show() {
    if (isShowing) return
    if (!hasOverlayPermission()) {
        return
    }
    lifecycleOwner = MyLifecycleOwner()
    try {
        weakReference.get()?.let { context ->
            windowManager.addView(ComposeView(context).apply {
                // 绑定生命周期到 ComposeView
                lifecycleOwner.attachToView(this)
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                lifecycleOwner.performRestore(null)
                lifecycleOwner.onCreate()
                lifecycleOwner.onStart()
                lifecycleOwner.onResume()
                setContent {
                    Text("悬浮窗已经打开")
                }
            }, layoutParams)
            isShowing = true
        }
    } catch (e: Exception) {
        lifecycleOwner.onPause()
        lifecycleOwner.onStop()
        lifecycleOwner.onDestroy()
        isShowing = false
        e.printStackTrace()
    }
}

关闭悬浮窗

Kotlin 复制代码
fun hide() {
    if (!isShowing) return
    try {
        lifecycleOwner.onPause()
        lifecycleOwner.onStop()
        windowManager.removeView(composeView)
        lifecycleOwner.onDestroy()
        isShowing = false
    } catch (e: Exception) {
        Log.e(TAG, "Error removing view: ${e.message}")
    }
}

总结

Compose 需要感知 ViewTreeLifecycleOwner。

在 androidx.activity:activity 依赖库中,androidx.activity.ComponentActivity 中同样为根 View 设置了 ViewTreeLifecycleOwner

androidx.activity.ComponentActivity#initializeViewTreeOwners

Kotlin 复制代码
open fun initializeViewTreeOwners() {
    window.decorView.setViewTreeLifecycleOwner(this)
    window.decorView.setViewTreeViewModelStoreOwner(this)
    window.decorView.setViewTreeSavedStateRegistryOwner(this)
    window.decorView.setViewTreeOnBackPressedDispatcherOwner(this)
    window.decorView.setViewTreeFullyDrawnReporterOwner(this)
}
Kotlin 复制代码
open class ComponentActivity() : androidx.core.app.ComponentActivity(),
    LifecycleOwner,
    ViewModelStoreOwner,
    SavedStateRegistryOwner {
}
相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui