点按并按下

许多可组合项都内置了对点按或点击的支持,并包含一个 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 向可组合项添加了实用功能。它们会在互动时显示视觉指示,响应悬停操作,并支持焦点、键盘和无障碍功能。但这种额外的行为并非总是可取的。

我们来看一下图片详情屏幕背景应该是半透明的,并且用户应能够点按该背景以关闭详情屏幕:

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

@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
        }
)