轻触和按压

许多可组合项都内置支持点击,并包含一个 onClick lambda 表达式。例如,您可以创建一个可点击的 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
        }
)