輕觸並按下

許多可組合項都內建輕觸或點擊支援功能,並包含 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 會為可組合項新增實用的功能。這些元件會在互動時顯示視覺指示,回應滑鼠懸停動作,並提供焦點、鍵盤和無障礙支援。但這項額外行為未必是理想的做法。

讓我們看看圖片詳細資料畫面。背景應為半透明,使用者應可輕觸該背景來關閉詳細資料畫面:

在這種情況下,背景不應在互動上提供任何視覺指示、不應回應滑鼠懸停,也不應可聚焦,且其對鍵盤和無障礙事件的回應與一般可組合項不同。您可以改為降至較低的抽象層級,直接使用 pointerInput 修飾符,並搭配 detectTapGestures 方法:clickable

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