点按并按下

许多可组合项都内置了对点按或点击的支持,并且包含 onClick lambda。例如,您可以创建一个可点击的 Surface, 包括所有适合与 Surface 交互的 Material Design 行为:

Surface(onClick = { /* handle click */ }) {
    Text("Click me!", Modifier.padding(24.dp))
}

但是,点击并不是用户与可组合项互动的唯一方式。当前页面 侧重于涉及单个指针的手势,此时 该指针对于处理该事件并不重要。以下 表格列出了以下类型的手势:

手势

说明

点按(或点击)

指针先向下再向上

点按两次

指针向下,向上,向下,向上

长按

指针按下,并保持较长时间

新闻

指针向下移动

响应点按或点击操作

clickable 是一个常用的修饰符,可让可组合项对 轻按或点击。此修饰符还添加其他功能,例如支持 鼠标和触控笔悬停,以及可自定义的视觉指示, 已按下。该修饰符响应“点击”而不是 还可通过键盘输入或在 使用无障碍服务

假设在图片网格中,当用户查看某个图片时,该图片会全屏显示 点击它:

您可以通过向网格中的每个项添加 clickable 修饰符来实现这一点 行为:

@Composable
private fun ImageGrid(photos: List<Photo>) {
    var activePhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
    LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
        items(photos, { it.id }) { photo ->
            ImageItem(
                photo,
                Modifier.clickable { activePhotoId = photo.id }
            )
        }
    }
    if (activePhotoId != null) {
        FullScreenImage(
            photo = photos.first { it.id == activePhotoId },
            onDismiss = { activePhotoId = null }
        )
    }
}

clickable 修饰符还会添加其他行为:

  • interactionSourceindication:当 用户点按该可组合项。如需了解如何自定义这些设置,请参阅处理用户 互动页面上。
  • 允许无障碍服务与元素交互,方法是设置 语义信息。
  • 通过允许焦点和按下操作,支持键盘或操纵杆互动 Enter 或方向键中心可进行互动。
  • 使元素可悬停,使其能够响应鼠标或触控笔悬停操作 。

长按可显示上下文菜单

combinedClickable 可让您在以下元素中添加点按两次或长按行为: 正常点击行为以外的功能。您可以使用 combinedClickable 来展示 在用户轻触并按住网格图片时触发的上下文菜单:

var contextMenuPhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
val haptics = LocalHapticFeedback.current
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
    items(photos, { it.id }) { photo ->
        ImageItem(
            photo,
            Modifier
                .combinedClickable(
                    onClick = { activePhotoId = photo.id },
                    onLongClick = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        contextMenuPhotoId = photo.id
                    },
                    onLongClickLabel = stringResource(R.string.open_context_menu)
                )
        )
    }
}
if (contextMenuPhotoId != null) {
    PhotoActionsSheet(
        photo = photos.first { it.id == contextMenuPhotoId },
        onDismissSheet = { contextMenuPhotoId = null }
    )
}

根据最佳实践,您应在用户 因此,该代码段包含 performHapticFeedback 调用。

点按纱罩以关闭可组合项

在上面的示例中,clickablecombinedClickable 添加 功能。它们以直观的方式呈现互动情况 响应悬停操作,并提供焦点、键盘和无障碍功能支持。但是 这种额外行为并不总是可取的。

我们来看看图片详情界面。背景应为半透明 并且用户应该能够点按该背景来关闭详情屏幕:

在这种情况下,背景上不应显示任何可见指示, 互动、不应响应悬停、不可聚焦,并且其 对键盘和无障碍事件的响应不同于 可组合项。您无需尝试调整 clickable 行为,而是可以 降低到较低的抽象级别,并直接使用 pointerInput 修饰符 与 detectTapGestures 方法结合使用:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) {
    val strClose = stringResource(R.string.close)
    Box(
        modifier
            // handle pointer input
            .pointerInput(onClose) { detectTapGestures { onClose() } }
            // handle accessibility services
            .semantics(mergeDescendants = true) {
                contentDescription = strClose
                onClick {
                    onClose()
                    true
                }
            }
            // handle physical keyboard input
            .onKeyEvent {
                if (it.key == Key.Escape) {
                    onClose()
                    true
                } else {
                    false
                }
            }
            // draw scrim
            .background(Color.DarkGray.copy(alpha = 0.75f))
    )
}

您将作为 pointerInput 修饰符的键传递 onClose lambda。本次 自动重新执行 lambda,确保调用正确的回调 当用户点按纱罩时触发。

点按两次即可缩放

有时,clickablecombinedClickable 提供的信息不足 以正确的方式响应互动。例如,可组合项 需要访问可组合项边界内的互动位置 事件。

我们再来看看图片详情屏幕。最佳做法是 可以通过双击放大图片:

如视频所示,缩放发生在点按位置周围 事件。当我们放大图片的左侧部分时,结果会有所不同 与正确的部分进行比较我们可以结合使用 pointerInput 修饰符 并使用 detectTapGestures 将点按位置整合到我们的 计算方式:

var zoomed by remember { mutableStateOf(false) }
var zoomOffset by remember { mutableStateOf(Offset.Zero) }
Image(
    painter = rememberAsyncImagePainter(model = photo.highResUrl),
    contentDescription = null,
    modifier = modifier
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = { tapOffset ->
                    zoomOffset = if (zoomed) Offset.Zero else
                        calculateOffset(tapOffset, size)
                    zoomed = !zoomed
                }
            )
        }
        .graphicsLayer {
            scaleX = if (zoomed) 2f else 1f
            scaleY = if (zoomed) 2f else 1f
            translationX = zoomOffset.x
            translationY = zoomOffset.y
        }
)