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

Jetpack Compose for Desktop 如何最优实现弹出菜单展示?

Compose跨平台PopupMenu在TextField右键/长按触发实现方案

1. 调整PopupMenu接口与实现

首先需要给现有PopupMenu新增偏移量参数,用来控制菜单弹出位置,修改后的代码如下:

通用接口调整

@Composable
expect fun PopupMenu(
    menuItems: List<String>,
    onClickCallbacks: List<() -> Unit>,
    showMenu: Boolean,
    onDismiss: () -> Unit,
    toggle: @Composable () -> Unit,
    popupOffset: Offset = Offset.Zero // 新增偏移量参数
)

桌面端实现调整

@Composable
actual fun PopupMenu(
    menuItems: List<String>,
    onClickCallbacks: List<() -> Unit>,
    showMenu: Boolean,
    onDismiss: () -> Unit,
    toggle: @Composable () -> Unit,
    popupOffset: Offset = Offset.Zero
) {
    val density = LocalDensity.current
    DropdownMenu(
        expanded = showMenu,
        onDismissRequest = { onDismiss() },
        // 把像素偏移转换为dp偏移传入DropdownMenu
        offset = with(density) { DpOffset(popupOffset.x.toDp(), popupOffset.y.toDp()) }
    ) {
        menuItems.forEachIndexed { index, item ->
            DropdownMenuItem(onClick = {
                onDismiss()
                onClickCallbacks[index]
            }) {
                Text(text = item)
            }
        }
    }
}

2. 实现OneCard组件的触发逻辑

新增状态控制菜单显隐、记录弹出位置,替换原有的selectable修饰符为手势监听修饰符,处理右键点击和长按事件,完整代码如下:

@Composable
fun OneCard(content: MutableState<TextFieldValue>, onValueChange: (TextFieldValue) -> Unit ) {
    // 控制菜单显隐的状态
    val showMenu = remember { mutableStateOf(false) }
    // 存储菜单弹出的位置坐标
    val menuOffset = remember { mutableStateOf(Offset.Zero) }
    // 系统剪贴板管理器,用来实现文本复制粘贴逻辑,按需引入
    val clipboardManager = LocalClipboardManager.current

    // 自定义菜单项和对应的点击回调,可根据业务需求替换
    val menuItems = listOf("复制", "剪切", "粘贴")
    val menuCallbacks = listOf(
        // 复制选中的文本
        {
            val selectedText = content.value.selection.normalized().let {
                content.value.text.substring(it.min, it.max)
            }
            clipboardManager.setText(AnnotatedString(selectedText))
        },
        // 剪切选中的文本
        {
            val selection = content.value.selection.normalized()
            val selectedText = content.value.text.substring(selection.min, selection.max)
            clipboardManager.setText(AnnotatedString(selectedText))
            onValueChange(
                content.value.copy(
                    text = content.value.text.removeRange(selection.min, selection.max),
                    selection = TextRange(selection.min)
                )
            )
        },
        // 粘贴剪贴板中的文本
        {
            val clipboardText = clipboardManager.getText()?.text ?: return@listOf
            val selection = content.value.selection
            onValueChange(
                content.value.copy(
                    text = content.value.text.replaceRange(selection.min, selection.max, clipboardText),
                    selection = TextRange(selection.min + clipboardText.length)
                )
            )
        }
    )

    Card(modifier = Modifier.draggable(DraggableState {  }, Orientation.Vertical)
        .padding(16.dp)) {
        // 将TextField作为触发组件传入PopupMenu
        PopupMenu(
            menuItems = menuItems,
            onClickCallbacks = menuCallbacks,
            showMenu = showMenu.value,
            onDismiss = { showMenu.value = false },
            popupOffset = menuOffset.value
        ) {
            OutlinedTextField(
                value = content.value,
                onValueChange = onValueChange,
                placeholder = { Text("Your thoughts?") },
                modifier = Modifier
                    .fillMaxWidth()
                    .pointerInput(Unit) {
                        // 处理长按触发逻辑
                        coroutineScope {
                            while (true) {
                                val downEvent = awaitPointerEventScope { awaitFirstDown() }
                                val longPressJob = launch {
                                    delay(500) // 长按触发延迟可自行调整,单位为毫秒
                                    menuOffset.value = downEvent.position
                                    showMenu.value = true
                                }
                                // 手势提前结束时取消长按触发
                                awaitPointerEventScope {
                                    waitForUpOrCancellation()
                                    longPressJob.cancel()
                                }
                            }
                        }
                        // 处理右键点击触发逻辑
                        detectTapGestures(
                            onSecondaryTap = { tapOffset ->
                                menuOffset.value = tapOffset
                                showMenu.value = true
                            }
                        )
                    }
            )
        }
    }
}

补充说明

  • 上述代码已经修复了原实现中placeholder参数的写法错误
  • 长按触发的延迟时间可以自行调整delay(500)中的数值
  • 如果不需要文本操作功能,可以直接替换menuItems和menuCallbacks为你需要的业务逻辑
  • 如果需要调整菜单弹出的位置偏移,直接修改menuOffset.value的数值即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 23:27:01