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

Jetpack Compose:combinedClickable与pointerInput共存问题

实现支持长按+向上拖拽的录音按钮(复刻Telegram/微信)

我需要实现一个复刻Telegram或微信录音按钮行为的组件:长按开始录音,向上拖拽可锁定录音(松开手也不会停止),点击则发送消息(输入框有内容时)或停止锁定的录音。但遇到两个问题:

测试1:Material3 FloatingActionButton长按失效

使用Material3的FloatingActionButton时,长按功能完全无响应,代码如下:

FloatingActionButton(
    modifier = Modifier
        .size(48.dp)
        .combinedClickable(
            onClick = {
                if (!textEmpty) {
                    onMessageChange(input.value.text)
                    input.value = TextFieldValue("")
                }

                if (recordingIsLock) {
                    stopRecord()
                }
            },
            onLongClick = {
                if (textEmpty) {
                    record()
                }
            }
        )
        .pointerInput(Unit) {
            detectVerticalDragGestures(
                onDragStart = {},
                onDragCancel = {},
                onDragEnd = {
                    if (!recordingIsLock) {
                        btnIndicatorHeight = 0F
                    }
                },
                onVerticalDrag = { change, dragAmount ->
                    // 判断输入框为空、向上拖拽、正在录音
                    if (textEmpty && change.position.y < 0 && dragAmount < 0 && isRecording) {
                        val aux = kotlin.math.abs(dragAmount)

                        btnIndicatorHeight += if (
                            !recordingIsLock &&
                            btnIndicatorHeight < 48
                        ) {
                            println("!!!!")
                            aux / 10
                        } else if (btnIndicatorHeight >= 48) {
                            if (!recordingIsLock) {
                                recordingIsLock = true
                            }
                            0F
                        } else {
                            0F
                        }
                    }
                })
        },
    onClick = {} // 空实现但会与combinedClickable冲突
) {
    Icon(
        modifier = Modifier.size(24.dp),
        tint = MaterialTheme.colorScheme.background,
        imageVector = if (textEmpty) Icons.Filled.Mic else Icons.Filled.Send,
        contentDescription = null
    )

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(btnIndicatorHeight.dp)
            .background(color = MaterialTheme.colorScheme.primary),
        contentAlignment = Alignment.Center
    ) {
        if (!recordingIsLock)
            Icon(
                tint = Color.White,
                imageVector = Icons.Outlined.Lock,
                contentDescription = null
            )
        else
            LottieAnimation(
                modifier = Modifier.size(30.dp),
                composition = recordingAnimationComposition,
                iterations = Int.MAX_VALUE
            )
    }
}

测试2:自定义组件拖拽行为失效

改用ElevatedCard自定义组件后,长按功能正常,但向上拖拽手势完全无触发,代码如下:

ElevatedCard(
    modifier = modifier
        .size(48.dp)
        .combinedClickable(
            onClick = {
                if (!textEmpty) {
                    onMessageChange(input.value.text)
                    input.value = TextFieldValue("")
                }

                if (recordingIsLock) {
                    stopRecord()
                }
            },
            onLongClick = {
                if (textEmpty) {
                    record()
                }
            }
        )
        .pointerInput(Unit) {
            detectVerticalDragGestures(
                onDragStart = {},
                onDragCancel = {},
                onDragEnd = {
                    if (!recordingIsLock) {
                        btnIndicatorHeight = 0F
                    }
                },
                onVerticalDrag = { change, dragAmount ->
                    // 判断输入框为空、向上拖拽、正在录音
                    if (textEmpty && change.position.y < 0 && dragAmount < 0 && isRecording) {
                        val aux = kotlin.math.abs(dragAmount)

                        btnIndicatorHeight += if (
                            !recordingIsLock &&
                            btnIndicatorHeight < 48
                        ) {
                            println("!!!!")
                            aux / 10
                        } else if (btnIndicatorHeight >= 48) {
                            if (!recordingIsLock) {
                                recordingIsLock = true
                            }
                            0F
                        } else {
                            0F
                        }
                    }
                })
        }
        .background(
            color = MaterialTheme.colorScheme.surface,
            shape = RoundedCornerShape(10.dp)
        )
) {
    Box(contentAlignment = Alignment.Center) {
        component()
    }
}

问题原因与解决方案

测试1失效原因

Material3的FloatingActionButton自带onClick参数,即使传空实现,也会消费点击/长按事件,导致Modifier.combinedClickable的手势被覆盖。

测试2失效原因

combinedClickable会消费所有触摸事件(包括拖拽),后续的pointerInput无法接收拖拽手势,导致拖拽逻辑完全不触发。

通用解决方案:统一用pointerInput处理所有手势

不要混用combinedClickable和pointerInput,在同一个pointerInput里同时处理长按、点击和拖拽手势,避免事件冲突。以下是适配后的完整代码(以Material3 FAB为例):

var isLongPressStarted by remember { mutableStateOf(false) }
var initialY by remember { mutableStateOf(0f) }

FloatingActionButton(
    modifier = Modifier
        .size(48.dp)
        .pointerInput(Unit) {
            detectTapGestures(
                onPress = {
                    // 按下时记录初始位置,启动长按检测
                    initialY = it.y
                    isLongPressStarted = false
                    val longPressTimeout = viewConfiguration.longPressTimeoutMillis
                    val pressJob = launch {
                        delay(longPressTimeout)
                        // 触发长按逻辑
                        if (textEmpty) {
                            record()
                            isLongPressStarted = true
                        }
                    }

                    try {
                        awaitRelease()
                        pressJob.cancel()
                        // 松开时,未触发长按则执行点击逻辑
                        if (!isLongPressStarted) {
                            if (!textEmpty) {
                                onMessageChange(input.value.text)
                                input.value = TextFieldValue("")
                            }
                            if (recordingIsLock) {
                                stopRecord()
                            }
                        } else {
                            // 长按后松开,未锁定录音则停止录音
                            if (!recordingIsLock) {
                                stopRecord()
                                btnIndicatorHeight = 0F
                            }
                        }
                    } catch (cancellation: CancellationException) {
                        pressJob.cancel()
                        // 拖拽取消时处理
                        if (isLongPressStarted && !recordingIsLock) {
                            stopRecord()
                            btnIndicatorHeight = 0F
                        }
                    }
                }
            )
        }
        .pointerInput(Unit) {
            detectVerticalDragGestures(
                onDragStart = { offset ->
                    // 仅长按开始后处理拖拽
                    if (isLongPressStarted) {
                        initialY = offset.y
                    }
                },
                onDragEnd = {
                    if (!recordingIsLock) {
                        btnIndicatorHeight = 0F
                    }
                },
                onVerticalDrag = { _, dragAmount ->
                    if (isLongPressStarted && textEmpty && isRecording) {
                        // 向上拖拽为负的dragAmount
                        if (dragAmount < 0 && !recordingIsLock) {
                            btnIndicatorHeight += kotlin.math.abs(dragAmount) / 10
                            if (btnIndicatorHeight >= 48) {
                                recordingIsLock = true
                            }
                        }
                        // 向下拖拽回到阈值内解锁(可选)
                        else if (dragAmount > 0 && recordingIsLock) {
                            btnIndicatorHeight -= kotlin.math.abs(dragAmount) / 10
                            if (btnIndicatorHeight < 48) {
                                recordingIsLock = false
                            }
                        }
                    }
                }
            )
        }
    // 移除自带的onClick,避免冲突
) {
    Icon(
        modifier = Modifier.size(24.dp),
        tint = MaterialTheme.colorScheme.background,
        imageVector = if (textEmpty) Icons.Filled.Mic else Icons.Filled.Send,
        contentDescription = if (textEmpty) "录音" else "发送消息"
    )

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(btnIndicatorHeight.dp)
            .background(color = MaterialTheme.colorScheme.primary),
        contentAlignment = Alignment.Center
    ) {
        if (!recordingIsLock)
            Icon(
                tint = Color.White,
                imageVector = Icons.Outlined.Lock,
                contentDescription = "锁定录音"
            )
        else
            LottieAnimation(
                modifier = Modifier.size(30.dp),
                composition = recordingAnimationComposition,
                iterations = Int.MAX_VALUE
            )
    }
}

关键优化点

  1. 用detectTapGestures的onPress回调统一处理长按和点击:通过延迟执行长按逻辑,松开时取消延迟任务区分点击与长按。
  2. 拖拽手势仅在长按开始后响应,避免误触发。
  3. 移除Material3 FAB自带的onClick参数,彻底解决事件冲突。
  4. 优化拖拽阈值判断,逻辑更清晰。

内容的提问来源于stack exchange,提问作者Eddy Yoel Fresno Hernández

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 15:00:19