Jeptpack Compose 官網(wǎng)教程學(xué)習(xí)筆記(五)番外-手勢(shì)

手勢(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.detectTapGesturesPointerInputScope.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 }
    )
}
響應(yīng)點(diǎn)按的界面元素示例

當(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)考慮使用 LazyColumnLazyRow 而不是使用這些 API。LazyColumnLazyRow 具有滾動(dòng)功能,它們的效率遠(yuǎn)高于滾動(dòng)修飾符,因?yàn)樗鼈儍H在需要時(shí)組合各個(gè)項(xiàng)

滾動(dòng)修飾符

verticalScrollhorizontalScroll 修飾符提供一種最簡(jiǎn)單的方法,可讓元素內(nèi)容邊界大于最大尺寸約束時(shí)滾動(dòng)元素。利用 verticalScrollhorizontalScroll 修飾符,無(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))
        }
    }
}
響應(yīng)滾動(dòng)手勢(shì)的簡(jiǎn)單垂直列表

借助 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())
    }
}
檢測(cè)手指按下手勢(shì)并顯示手指位置數(shù)值

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、scrollableLazy APITextField。這意味著,當(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)
                )
            }
        }
    }
}
嵌套垂直滾動(dòng)界面
協(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)的界面元素

滑動(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)
        )
    }
}
響應(yīng)滑動(dòng)手勢(shì)的界面元素

多點(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()
    )
}
響應(yīng)多點(diǎn)觸控手勢(shì)(平移、縮放和旋轉(zhuǎn))的界面元素

如果您需要將縮放、平移和旋轉(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)角度

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容