You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Composable动态适配子组件并在应用上层悬浮显示?

问题描述

我正在尝试通过MotionLayout实现一个Composable的最小化效果,使其悬浮在当前应用原有Composable的上方。目前仅完成了最小化功能,但还需要解决两个核心问题:

  • 父Composable需要根据子组件的尺寸动态调整自身大小
  • 让该Composable能够悬浮显示在当前运行应用的上层

实现代码

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.constraintlayout.compose.ConstraintSet
import androidx.constraintlayout.compose.ExperimentalMotionApi
import androidx.constraintlayout.compose.MotionLayout
import androidx.constraintlayout.compose.MotionLayoutDebugFlags
import androidx.constraintlayout.compose.layoutId
import java.util.EnumSet

@OptIn(ExperimentalMotionApi::class)
@Composable
fun TestMobilePlayerScreen(
    modifier: Modifier = Modifier
) {
    var animate by remember { mutableStateOf(false) }
    val progress by animateFloatAsState(targetValue = if (animate) 1f else 0f, animationSpec = tween(1_000))

    MotionLayout(
        start = ConstraintSet(
            """
            {
              videoPlayer: {
                width: "spread",
                height: "16:9",
                top: ['parent', 'top'],
                start: ['parent', 'start'],
                end: ['parent', 'end']
              },
              programDetails: {
                width: "spread",
                height: "spread",
                top: ['videoPlayer', 'bottom'],
                bottom: ['parent', 'bottom'],
                start: ['videoPlayer', 'start'],
                end: ['videoPlayer', 'end']
              }
            }"""
        ),
        end = ConstraintSet(
            """
            {
              videoPlayer: {
                width: 200,
                height: "16:9",
                bottom: ['parent', 'bottom', 16],
                end: ['parent', 'end', 16],
              },
              programDetails: {
                width: 200,
                height: 0,
                top: ['videoPlayer', 'bottom'],
                start: ['videoPlayer', 'start'],
                end: ['videoPlayer', 'end']
              }
            }"""
        ),
        progress = progress,
        debug = EnumSet.of(MotionLayoutDebugFlags.SHOW_ALL),
        modifier = modifier
            .fillMaxSize()
            .background(Color.Transparent),
    ) {
        Box(
            modifier = Modifier
                .layoutId("videoPlayer")
                .background(color = Color.Black)
                .clickable { animate = !animate }
        )
        Column(
            Modifier
                .layoutId("programDetails")
                .background(color = Color.Green)
        ) {
        }
    }
}

状态展示

  • 初始状态:打开该Composable时的状态
  • 过渡中状态:Composable向最终状态过渡的中间状态
  • 最终状态:Composable的最终状态

解决方案

1. 父Composable动态适配子组件尺寸

当前MotionLayout使用.fillMaxSize()强制占满屏幕,导致父组件无法跟随子组件收缩。调整方案如下:

  • 替换.fillMaxSize()为.wrapContentSize(),让父组件尺寸由子内容决定
  • 修改结束状态的ConstraintSet,明确父容器的宽高为包裹内容

修改后的MotionLayout代码片段

MotionLayout(
    start = startConstraintSet,
    end = endConstraintSet,
    progress = progress,
    debug = EnumSet.of(MotionLayoutDebugFlags.SHOW_ALL),
    modifier = modifier
        .wrapContentSize() // 改为包裹子内容
        .background(Color.Transparent),
)

调整后的结束状态ConstraintSet

{
  "parent": {
    "width": "wrap",
    "height": "wrap"
  },
  "videoPlayer": {
    "width": 200,
    "height": "16:9",
    "bottom": ['parent', 'bottom', 16],
    "end": ['parent', 'end', 16],
  },
  "programDetails": {
    "width": 200,
    "height": 0,
    "top": ['videoPlayer', 'bottom'],
    "start": ['videoPlayer', 'start'],
    "end": ['videoPlayer', 'end']
  }
}

2. 实现Composable悬浮在应用上层

要让Composable悬浮于所有应用内容上方,需要借助Android的悬浮窗机制:

步骤1:添加权限声明

在AndroidManifest.xml中添加悬浮窗权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

步骤2:动态请求权限(Android 6.0+)

在Activity中检查并请求悬浮窗权限:

private const val OVERLAY_PERMISSION_REQUEST_CODE = 1001

if (!Settings.canDrawOverlays(this)) {
    val intent = Intent(
        Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
        Uri.parse("package:$packageName")
    )
    startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE)
}

步骤3:通过WindowManager添加悬浮Composable

在Activity中创建悬浮窗口并加载目标Composable:

class MainActivity : ComponentActivity() {
    private lateinit var windowManager: WindowManager
    private lateinit var floatingView: View

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        // 配置悬浮窗参数
        val layoutParams = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            } else {
                WindowManager.LayoutParams.TYPE_PHONE
            },
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 
            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
            PixelFormat.TRANSLUCENT
        )
        layoutParams.gravity = Gravity.BOTTOM or Gravity.END // 初始位置为右下角

        // 创建加载Compose的View
        floatingView = ComposeView(this).apply {
            setContent {
                TestMobilePlayerScreen()
            }
        }

        // 添加到WindowManager
        windowManager.addView(floatingView, layoutParams)
    }

    override fun onDestroy() {
        super.onDestroy()
        // 移除悬浮窗
        windowManager.removeView(floatingView)
    }
}

内容的提问来源于stack exchange,提问作者Omkar Amberkar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 01:25:40