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

Android 14 TYPE_APPLICATION_OVERLAY悬浮按钮首次点击位置闪回问题

Android 14悬浮气泡拖动后首次点击闪回问题

代码更新[2024-11-28]

问题描述

将悬浮气泡从位置A拖动到位置B后,首次点击位置B处的气泡时,它会短暂闪回/跳转到位置A,随后回到位置B。该问题仅在移动到新位置后的首次点击时出现。

环境信息

  • Android Studio Ladybug | 2024.2.1 Patch 2
  • Compose版本: 2024.04.01
  • 测试设备:
    • Android 14(问题复现)
    • Android 13及以下版本(正常工作)

相关代码

class FloatingBubbleService : Service() {
    private lateinit var windowManager: WindowManager
    private var bubbleView: ComposeView? = null
    
    // 记录最后确认的位置
    private var lastKnownX = 0
    private var lastKnownY = 0

    private val viewOwner = object : LifecycleOwner, SavedStateRegistryOwner {
        val lifecycleRegistry = LifecycleRegistry(this)
        private val savedStateRegistryController = SavedStateRegistryController.create(this).apply {
            // 立即初始化SavedStateRegistry
            performRestore(Bundle())
        }
        init {
            lifecycleRegistry.currentState = Lifecycle.State.CREATED
        }

        override val lifecycle: Lifecycle
            get() =  lifecycleRegistry

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

    private val params = WindowManager.LayoutParams(
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 
            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or 
            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
        PixelFormat.TRANSLUCENT
    ).apply {
        gravity = Gravity.TOP or Gravity.START
        x = 0
        y = 0
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onCreate() {
        super.onCreate()
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        setupFloatingBubble()
        viewOwner.lifecycleRegistry.currentState = Lifecycle.State.RESUMED

    }

    private fun setupFloatingBubble() {
        bubbleView = ComposeView(this).apply {
            setViewTreeLifecycleOwner(viewOwner)
            setViewTreeSavedStateRegistryOwner(viewOwner)
            
            var initialX = 0
            var initialY = 0
            var initialTouchX = 0f
            var initialTouchY = 0f
            var isDragging = false

            setContent {
                var isMenuVisible by remember{ mutableStateOf(false)}
                FloatingBubbleTheme {
                    FloatingBubbleContent(
                        isMenuVisible = isMenuVisible,
                        onBubbleClick = { /* 不再需要 */ },
                        interceptTouchEvents = false
                    )
                }
                setOnTouchListener { view, event -> 
                    Log.i(LOGTAG, "接收到触摸事件: ${event.action}")
                    when (event.action) {
                        MotionEvent.ACTION_DOWN -> {
                            Log.i(LOGTAG, "ACTION_DOWN++")
                            isDragging = false
                            initialX = params.x
                            initialY = params.y
                            initialTouchX = event.rawX
                            initialTouchY = event.rawY
                            true
                        }

                        MotionEvent.ACTION_MOVE -> {
                            if (!isDragging && (Math.abs(event.rawX - initialTouchX) > 10 ||
                                Math.abs(event.rawY - initialTouchY) > 10)) {
                                isDragging = true
                            }

                            if (isDragging) {
                                val newX = initialX + (event.rawX - initialTouchX).toInt()
                                val newY = initialY + (event.rawY - initialTouchY).toInt()
                                Log.i(LOGTAG, "移动到 ${newX}: ${newY}")
                                params.x = newX
                                params.y = newY

                                try {
                                    windowManager.updateViewLayout(view, params)
                                } catch (e: Exception) {
                                    e.printStackTrace()
                                }
                            }
                            true
                        }

                        MotionEvent.ACTION_UP -> {
                            if (!isDragging) {
                                // 使用可变状态切换菜单可见性
                                isMenuVisible = !isMenuVisible
                            }
                            true
                        }
                        else -> false
                    }
                }
            }
        }

        try {
            windowManager.addView(bubbleView, params)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        bubbleView?.let { windowManager.removeView(it) }
        viewOwner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED

    }
}

预期行为

悬浮气泡被点击时应停留在位置B,无位置闪回现象。

Android 14上的实际行为

  1. 将悬浮气泡从位置A拖动到B
  2. 点击位置B处的气泡
  3. 气泡短暂闪回至位置A
  4. 气泡回到位置B

已尝试方案

  1. 不同WindowManager.LayoutParams标志组合
  2. 在onDraw中检查位置更新
  3. 验证触摸坐标与窗口参数

疑问

这是Android 14 WindowManager的已知问题吗?有没有可避免点击悬浮视图时出现位置闪回的解决方法?

感谢任何帮助或见解!

更新[2024-11-26]

@Composable
fun FloatingBubbleContent(
    isMenuVisible: Boolean,
    onBubbleClick: () -> Unit,
    interceptTouchEvents: Boolean = false
) {
    Box(
        modifier = Modifier
            .width(174.dp)  // 固定宽度
            .height(56.dp)  // 固定高度
            .then(
                if (interceptTouchEvents) {
                    Modifier.clickable(onClick = onBubbleClick)
                } else {
                    Modifier
                }
            )
    ) {
        Box(
            modifier = Modifier
                .size(56.dp)  // 固定气泡大小
                .align(Alignment.CenterEnd)  // 气泡靠右对齐
        ) {
            BubbleIcon()
        }
        
        AnimatedMenu(
            isVisible = isMenuVisible,
            modifier = Modifier.align(Alignment.CenterStart)
        )
    }
}

@Composable
private fun AnimatedMenu(
    isVisible: Boolean,
    modifier: Modifier = Modifier
) {
    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn() + slideIn(initialOffset = { IntOffset(-50, 0) }),
        exit = fadeOut() + slideOut(targetOffset = { IntOffset(-50, 0) }),
        modifier = modifier
    ) {
        Box(
            modifier = Modifier
                .width(118.dp)  // 70dp + 48dp
                .height(56.dp)  // 匹配气泡高度
        ) {
            Column(
                modifier = Modifier
                    .offset(x = (-70).dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                MenuButton(
                    icon = Icons.Rounded.Call,
                    backgroundColor = Color(0xFF4CAF50),
                    onClick = { /* 处理电话事件 */ }
                )

                MenuButton(
                    icon = Icons.Rounded.Email,
                    backgroundColor = Color(0xFF2196F3),
                    onClick = { /* 处理邮件事件 */ }
                )
            }
        }
    }
}

更新[2024-11-28]

清单文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FloatableIcon"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name="com.example.floatableicon.service.FloatingBubbleService"
            android:enabled="true"
            android:exported="false" />
    </application>

</manifest>

内容的提问来源于stack exchange,提问作者Sameer Meena

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:54:54