手勢(shì)
Compose 提供了多種 API,可幫助您檢測(cè)用戶(hù)互動(dòng)生成的手勢(shì)。API 涵蓋各種用例:
- 其中一些級(jí)別較高,旨在覆蓋最常用的手勢(shì)。例如,
clickable修飾符可用于輕松檢測(cè)點(diǎn)擊,此外它還提供無(wú)障礙功能,并在點(diǎn)按時(shí)顯示視覺(jué)指示(例如漣漪) - 還有一些不太常用的手勢(shì)檢測(cè)器,它們?cè)谳^低級(jí)別提供更大的靈活性,例如
PointerInputScope.detectTapGestures或PointerInputScope.detectDragGestures,但不提供額外功能
點(diǎn)擊
clickable 修飾符允許應(yīng)用檢測(cè)對(duì)已應(yīng)用該修飾符的元素的點(diǎn)擊
@Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// 你想要實(shí)現(xiàn)點(diǎn)擊效果的組件
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}

當(dāng)需要更大靈活性時(shí),您可以通過(guò) pointerInput 修飾符提供點(diǎn)按手勢(shì)檢測(cè)器:
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* 按下事件,所有手勢(shì)事件的開(kāi)始 */ },
onDoubleTap = { /* 雙擊事件 */ },
onLongPress = { /* 長(zhǎng)按事件 */ },
onTap = { /* 點(diǎn)擊事件 */ }
)
}
在使用
pointerInput必須傳遞至少一個(gè)Key,當(dāng)Key發(fā)生變化時(shí)會(huì)重新調(diào)用pointerInput,若不想其發(fā)生變化傳遞一個(gè)Unit即可fun Modifier.pointerInput( key1: Any?, block: suspend PointerInputScope.() -> Unit ) { ... }可以看到
block為掛起函數(shù),因?yàn)榈却謩?shì)必然是異步事件,需要使用協(xié)程等待手勢(shì)事件的發(fā)生
PointerInputScope內(nèi)我們可以在awaitPointerEventScope協(xié)程作用域內(nèi)檢測(cè)手勢(shì)事件發(fā)生,詳情請(qǐng)看官網(wǎng)介紹
當(dāng)然級(jí)別高的手勢(shì)檢測(cè)器內(nèi)部都是通過(guò)低級(jí)別的手勢(shì)檢測(cè)器完成,比如
clickable內(nèi)部是通過(guò)封裝pointerInput完成點(diǎn)擊事件的檢測(cè)fun Modifier.clickable( interactionSource: MutableInteractionSource, indication: Indication?, enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onClick: () -> Unit ) = composed( factory = { val onClickState = rememberUpdatedState(onClick) ... val gesture = Modifier.pointerInput(interactionSource, enabled) { detectTapAndPress( onPress = { offset -> if (enabled) { handlePressInteraction( offset, interactionSource, pressedInteraction, delayPressInteraction ) } }, onTap = { if (enabled) onClickState.value.invoke() } ) } Modifier .then( ... ).genericClickableWithoutGesture( gestureModifiers = gesture, interactionSource = interactionSource, indication = indication, enabled = enabled, onClickLabel = onClickLabel, role = role, onLongClickLabel = null, onLongClick = null, onClick = onClick ) }, inspectorInfo = ... )
滾動(dòng)
注意:如果您想要顯示項(xiàng)列表,請(qǐng)考慮使用
LazyColumn和LazyRow而不是使用這些 API。LazyColumn和LazyRow具有滾動(dòng)功能,它們的效率遠(yuǎn)高于滾動(dòng)修飾符,因?yàn)樗鼈儍H在需要時(shí)組合各個(gè)項(xiàng)
滾動(dòng)修飾符
verticalScroll 和 horizontalScroll 修飾符提供一種最簡(jiǎn)單的方法,可讓元素內(nèi)容邊界大于最大尺寸約束時(shí)滾動(dòng)元素。利用 verticalScroll 和 horizontalScroll 修飾符,無(wú)需轉(zhuǎn)換或偏移內(nèi)容
@Composable
fun ScrollBoxes() {
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.verticalScroll(rememberScrollState())
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}

借助
ScrollState,您可以更改滾動(dòng)位置或獲取當(dāng)前狀態(tài)
scrollable修飾符
scrollable 修飾符與滾動(dòng)修飾符不同,區(qū)別在于 scrollable 可檢測(cè)滾動(dòng)手勢(shì),但不會(huì)偏移其內(nèi)容。必須有 ScrollableState參數(shù),此修飾符才能正常工作
構(gòu)造 ScrollableState 時(shí),必須提供一個(gè) consumeScrollDelta 函數(shù),該函數(shù)將在每個(gè)滾動(dòng)步驟調(diào)用(通過(guò)手勢(shì)輸入、流暢滾動(dòng)或快速滑動(dòng)),并且增量以像素為單位。該函數(shù)會(huì)返回所消耗的滾動(dòng)距離,以確保在存在具有 scrollable 修飾符的嵌套元素時(shí),可以正確傳播相應(yīng)事件
以下代碼段可檢測(cè)手勢(shì)并顯示偏移量的數(shù)值,但不會(huì)偏移任何元素:
@Composable
fun ScrollableSample() {
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
//不是 rememberScrollState,而且rememberScrollState也可以作為這里的參數(shù)
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
// 可以通過(guò)offset實(shí)現(xiàn)滾動(dòng)效果
// .offset(0.dp, LocalDensity.current.run {
// offset.toDp()
// })
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}

scrollable修飾符不會(huì)影響它所應(yīng)用到的元素的布局。這意味著,對(duì)元素布局或其子級(jí)進(jìn)行的任何更改都必須通過(guò)由ScrollableState提供的增量進(jìn)行處理。另外請(qǐng)務(wù)必注意,scrollable不會(huì)考慮子級(jí)的布局,這意味著它無(wú)需測(cè)量子級(jí),即可傳播滾動(dòng)增量
嵌套滾動(dòng)
Compose 支持嵌套滾動(dòng),可讓多個(gè)元素對(duì)一個(gè)滾動(dòng)手勢(shì)做出回應(yīng)。典型的嵌套滾動(dòng)示例是在一個(gè)列表中嵌套另一個(gè)列表
自動(dòng)嵌套滾動(dòng)
簡(jiǎn)單的嵌套滾動(dòng)無(wú)需您執(zhí)行任何操作。啟動(dòng)滾動(dòng)操作的手勢(shì)會(huì)自動(dòng)從子級(jí)傳播到父級(jí),這樣一來(lái),當(dāng)子級(jí)無(wú)法進(jìn)一步滾動(dòng)時(shí),手勢(shì)就會(huì)由其父元素處理
部分 Compose 組件和修飾符原生支持自動(dòng)嵌套滾動(dòng),包括:verticalScroll、horizontalScroll、scrollable、Lazy API 和TextField。這意味著,當(dāng)用戶(hù)滾動(dòng)嵌套組件的內(nèi)部子級(jí)時(shí),之前的修飾符會(huì)將滾動(dòng)增量傳播到支持嵌套滾動(dòng)的父級(jí)
以下示例顯示的元素應(yīng)用了 verticalScroll 修飾符,而其所在的容器同樣應(yīng)用了 verticalScroll 修飾符
//漸變色
val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
modifier = Modifier
.background(Color.LightGray)
.verticalScroll(rememberScrollState())
.padding(32.dp)
) {
Column {
repeat(6) {
Box(
modifier = Modifier
.height(128.dp)
.verticalScroll(rememberScrollState())
) {
Text(
"Scroll here",
modifier = Modifier
.border(12.dp, Color.DarkGray)
.background(brush = gradient)
.padding(24.dp)
.height(150.dp)
)
}
}
}
}

協(xié)調(diào)滾動(dòng)
如果需要在多個(gè)元素之間創(chuàng)建高級(jí)協(xié)調(diào)滾動(dòng),可以使用 nestedScroll 修飾符定義嵌套滾動(dòng)層次結(jié)構(gòu)來(lái)提高靈活性。 請(qǐng)注意,一些組件內(nèi)置對(duì)嵌套滾動(dòng)的支持。 我們可以使用 nestedScroll 向其他組件(包括自定義組件)提供此類(lèi)支持
拖動(dòng)
draggable 修飾符是向單一方向拖動(dòng)手勢(shì)的高級(jí)入口點(diǎn),并且會(huì)報(bào)告拖動(dòng)距離(以像素為單位)
請(qǐng)務(wù)必注意,此修飾符與 scrollable 類(lèi)似,僅檢測(cè)手勢(shì)。若需要保存狀態(tài)并在屏幕上表示,例如通過(guò) offset 修飾符移動(dòng)元素:
var offsetX by remember { mutableStateOf(0f) }
Text(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
),
text = "Drag me!"
)
如果您需要控制整個(gè)拖動(dòng)手勢(shì),請(qǐng)考慮改為通過(guò) pointerInput 修飾符使用拖動(dòng)手勢(shì)檢測(cè)器
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}

滑動(dòng)
我們可以使用 swipeable 修飾符拖動(dòng)元素,釋放后,這些元素通常朝一個(gè)方向定義的兩個(gè)或多個(gè)錨點(diǎn)呈現(xiàn)動(dòng)畫(huà)效果。其常見(jiàn)用途有滑動(dòng)關(guān)閉、圖塊滑動(dòng)驗(yàn)證等
請(qǐng)務(wù)必注意,此修飾符不會(huì)移動(dòng)元素,而只檢測(cè)手勢(shì)。您需要保存狀態(tài)并在屏幕上表示,例如通過(guò) offset 修飾符移動(dòng)元素
在 swipeable 修飾符中必須提供可滑動(dòng)狀態(tài),且該狀態(tài)可以通過(guò) rememberSwipeableState() 創(chuàng)建和記住。此狀態(tài)還提供了一組有用的方法,用于以程序化方式為錨點(diǎn)添加動(dòng)畫(huà)效果,同時(shí)為屬性添加動(dòng)畫(huà)效果,以觀察拖動(dòng)進(jìn)度
可以將滑動(dòng)手勢(shì)配置為具有不同的閾值類(lèi)型,例如 FixedThreshold(Dp) 和 FractionalThreshold(Float),并且對(duì)于每個(gè)錨點(diǎn)的起始與終止組合,它們可以是不同的
閾值:當(dāng)滑動(dòng)元素超過(guò)閾值指定的值,此時(shí)松開(kāi)手指,滑動(dòng)元素會(huì)自動(dòng)滑動(dòng)至同方向上的下一個(gè)anchor(錨點(diǎn))
為了獲得更大的靈活性,您可以配置滑動(dòng)越過(guò)邊界時(shí)的 resistance,還可以配置 velocityThreshold,即使尚未達(dá)到位置 thresholds,velocityThreshold 仍將以動(dòng)畫(huà)方式向下一個(gè)狀態(tài)滑動(dòng)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeableSample() {
val width = 96.dp
val squareSize = 48.dp
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
// Maps anchor points (in px) to states
// 錨點(diǎn)元素不能空,而且value也必須唯一
val anchors = mapOf(0f to 0, sizePx to 1)
Box(
modifier = Modifier
.width(width)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(squareSize)
.background(Color.DarkGray)
)
}
}

多點(diǎn)觸控:平移、縮放、旋轉(zhuǎn)
如需檢測(cè)用于平移、縮放和旋轉(zhuǎn)的多點(diǎn)觸控手勢(shì),您可以使用 transformable 修飾符。此修飾符本身不會(huì)轉(zhuǎn)換元素,只會(huì)檢測(cè)手勢(shì)
@Composable
fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}

如果您需要將縮放、平移和旋轉(zhuǎn)與其他手勢(shì)結(jié)合使用,可以使用 PointerInputScope.detectTransformGestures 檢測(cè)器
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
)
centroid:質(zhì)點(diǎn),旋轉(zhuǎn)中心
pan:位移量
zoom:放大系數(shù)
rotation:旋轉(zhuǎn)角度