<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Jetpack Compose(7)——觸摸反饋

      本文介紹 Jetpack Compose 中的手勢處理。

      官方文檔的對 Compose 中的交互做了分類,比如指針輸入、鍵盤輸入等。本文主要是介紹指針輸入,類比傳統 View 體系中的事件分發。

      說明:在 Compose 中,手勢處理是通過 Modifier 實現的。這里,有人可能要反駁,Button 這個可組合項,就是專門用來響應點擊事件的,莫慌,接著往下看。

      一、點按手勢

      1.1 Modifier.clickable

      fun Modifier.clickable(
          enabled: Boolean = true,
          onClickLabel: String? = null,
          role: Role? = null,
          onClick: () -> Unit
      )
      

      Clickable 修飾符用來監聽組件的點擊操作,并且當點擊事件發生時會為被點擊的組件施加一個波紋漣漪效果動畫的蒙層。

      Clickable 修飾符使用起來非常簡單,在絕大多數場景下我們只需要傳入 onClick 回調即可,用于處理點擊事件。當然你也可以為 enable 參數設置為一個可變狀態,通過狀態來動態控制啟用點擊監聽。

      @Composable
      fun ClickDemo() {
        var enableState by remember {
          mutableStateOf<Boolean>(true)
        }
        Box(modifier = Modifier
            .size(200.dp)
            .background(Color.Green)
            .clickable(enabled = enableState) {
              Log.d(TAG, "發生單擊操作了~")
            }
        )
      }
      

      這里可以回答上面的問題,關于 Button 可組合項,我們看下 Button 的源碼:

      @OptIn(ExperimentalMaterialApi::class)
      @Composable
      fun Button(
          onClick: () -> Unit,
          modifier: Modifier = Modifier,
          enabled: Boolean = true,
          interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
          elevation: ButtonElevation? = ButtonDefaults.elevation(),
          shape: Shape = MaterialTheme.shapes.small,
          border: BorderStroke? = null,
          colors: ButtonColors = ButtonDefaults.buttonColors(),
          contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
          content: @Composable RowScope.() -> Unit
      ) {
          val contentColor by colors.contentColor(enabled)
          Surface(
              onClick = onClick,
              modifier = modifier.semantics { role = Role.Button },
              enabled = enabled,
              shape = shape,
              color = colors.backgroundColor(enabled).value,
              contentColor = contentColor.copy(alpha = 1f),
              border = border,
              elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
              interactionSource = interactionSource,
          ) {
              // ... 省略其它代碼
          }
      }
      

      實際是 surface 組件響應的 onClick 事件。

      @ExperimentalMaterialApi
      @Composable
      fun Surface(
          onClick: () -> Unit,
          modifier: Modifier = Modifier,
          enabled: Boolean = true,
          shape: Shape = RectangleShape,
          color: Color = MaterialTheme.colors.surface,
          contentColor: Color = contentColorFor(color),
          border: BorderStroke? = null,
          elevation: Dp = 0.dp,
          interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
          content: @Composable () -> Unit
      ) {
          val absoluteElevation = LocalAbsoluteElevation.current + elevation
          CompositionLocalProvider(
              LocalContentColor provides contentColor,
              LocalAbsoluteElevation provides absoluteElevation
          ) {
              Box(
                  modifier = modifier
                      .minimumInteractiveComponentSize()
                      .surface(
                          shape = shape,
                          backgroundColor = surfaceColorAtElevation(
                              color = color,
                              elevationOverlay = LocalElevationOverlay.current,
                              absoluteElevation = absoluteElevation
                          ),
                          border = border,
                          elevation = elevation
                      )
                      .clickable(
                          interactionSource = interactionSource,
                          indication = rememberRipple(),
                          enabled = enabled,
                          onClick = onClick
                      ),
                  propagateMinConstraints = true
              ) {
                  content()
              }
          }
      }
      

      我們看到,surface 的實現又是基于 Box, 最終 Box 是通過 Modifier.clickable 響應點擊事件的。

      1.2 Modifier.combinedClickable

      fun Modifier.combinedClickable(
        enabled: Boolean = true,
        onClickLabel: String? = null,
        role: Role? = null,
        onLongClickLabel: String? = null,
        onLongClick: (() -> Unit)? = null,
        onDoubleClick: (() -> Unit)? = null,
        onClick: () -> Unit
      )
      

      除了點擊事件,我們經常使用到的還有雙擊、長按等手勢需要響應,Compose 提供了 Modifier.combinedClickable 用來響應對于長按點擊、雙擊等復合類點擊手勢,與 Clickable 修飾符一樣,他同樣也可以監聽單擊手勢,并且也會為被點擊的組件施加一個波紋漣漪效果動畫的蒙層。

      @Composable
      fun CombinedClickDemo() {
        var enableState by remember {
          mutableStateOf<Boolean>(true)
        }
        Box(modifier = Modifier
          .size(200.dp)
          .background(Color.Green)
          .combinedClickable(
            enabled = enableState,
            onLongClick = {
              Log.d(TAG, "發生長按點擊操作了~")
            },
            onDoubleClick = {
              Log.d(TAG, "發生雙擊操作了~")
            },
            onClick = {
              Log.d(TAG, "發生單擊操作了~")
            }
          )
        )
      }
      

      二、滾動手勢

      這里所說的滾動,是指可組合項的內容發生滾動,如果想要顯示列表,請考慮使用 LazyXXX 系列組件。

      2.1 滾動修飾符 Modifier.verticalScorll / Modifier.horizontalScorll

      fun Modifier.verticalScroll(
          state: ScrollState,
          enabled: Boolean = true,
          flingBehavior: FlingBehavior? = null,
          reverseScrolling: Boolean = false
      )
      
      • state 表示滾動狀態
      • enabled 表示是否啟用 / 禁用該滾動
      • flingBehavior 參數表示拖動結束之后的 fling 行為,默認為 null, 會使用 ScrollableDefaults. flingBehavior 策略。
      • reverseScrolling, false 表示 ScrollState 為 0 時對應最頂部 top, ture 表示 ScrollState 為 0 時對應底部 bottom。
        注意:這個反轉不是指滾動方向反轉,而是對 state 的反轉,當 state 為 0 時,即處于列表最底部

      大多數場景,我們只需要傳入 state 即可。

      @Composable
      fun TestScrollBox() {
          Column(
              modifier = Modifier
                  .background(Color.LightGray)
                  .wrapContentWidth()
                  .height(80.dp)
                  .verticalScroll(
                      state = rememberScrollState()
                  )
          ) {
              repeat(20) {
                  Text("item --> $it")
              }
          }
      }
      

      借助 ScrollState,您可以更改滾動位置或獲取其當前狀態。比如滾動到初始位置,則可以調用

      state.scrollTo(0)
      

      對于 Modifier.horizontalScorll,從命名可以看出,Modifier.verticalScorll 用來實現垂直方向滾動,而 Modifier.horizontalScorll 用來實現水平方向的滾動。這里不再贅述了。

      fun Modifier.horizontalScroll(
          state: ScrollState,
          enabled: Boolean = true,
          flingBehavior: FlingBehavior? = null,
          reverseScrolling: Boolean = false
      )
      

      2.2 可滾動修飾符 Modifier.scrollable

      scrollable 修飾符與滾動修飾符不同,scrollable 會檢測滾動手勢并捕獲增量,但不會自動偏移其內容。而是通過 ScrollableState 委派給用戶,而這是此修飾符正常運行所必需的。

      構建 ScrollableState 時,您必須提供一個 consumeScrollDelta 函數,該函數將在每個滾動步驟(通過手勢輸入、平滑滾動或快速滑動)調用,并且增量以像素為單位。此函數必須返回使用的滾動距離量,以確保在存在具有 scrollable 修飾符的嵌套元素時正確傳播事件。

      注意:scrollable 修飾符不會影響應用該修飾符的元素的布局。這意味著,對元素布局或其子項所做的任何更改都必須通過 ScrollableState 提供的增量進行處理。另外還需要注意的是,scrollable 并不考慮子項的布局,這意味著它不需要測量子項即可傳播滾動增量。

      @Composable
      fun rememberScrollableState(consumeScrollDelta: (Float) -> Float): ScrollableState {
          val lambdaState = rememberUpdatedState(consumeScrollDelta)
          return remember { ScrollableState { lambdaState.value.invoke(it) } }
      }
      

      看個具體例子:

      @Composable
      fun TestScrollableBox() {
          var offsetY by remember {
              mutableFloatStateOf(0f)
          }
          Column(modifier = Modifier
              .background(Color.LightGray)
              .wrapContentWidth()
              .offset(
                  y = with(LocalDensity.current) {
                      offsetY.toDp()
                  }
              )
              .scrollable(
                  orientation = Orientation.Vertical,
                  state = rememberScrollableState { consumeScrollDelta ->
                      offsetY += consumeScrollDelta
                      consumeScrollDelta
                  }
              )) {
              repeat(20) {
                  Text("item --> $it")
              }
          }
      }
      

      運行效果如下:

      其實很好理解,rememberScrollableState 提供了滾動的偏移量,需要自己對偏移量進行處理,并且需要指定消費。

      除了自己實現 rememberScrollableState 之外,也可以用前面的 rememberScrollState,它提供了一個默認的實現,將滾動數據存儲在 ScrollState 的 value 中,并消費掉所有的滾動距離。但是 ScrollState 的值的范圍是大于 0 的,無法出現負數。

      @Stable
      class ScrollState(initial: Int) : ScrollableState {
      
      }
      

      可以看到 ScrollState 實現了 ScrollableState 這個接口。

      另外,Modifier.scrollable 可滾動修飾符需要指定滾動方向垂直或者水平。相對而言,該修飾符處于更低級別,靈活性更強,而上一小節講到的滾動修飾符則是基于 Modifier.scrollable 實現的。理解好兩者的區別,才能在實際開發中選擇合適的 API。

      三、 拖動手勢

      3.1 Modifier.draggable

      Modifier.draggable 修飾符只能監聽水平或者垂直方向的拖動偏移。

      fun Modifier.draggable(
          state: DraggableState,
          orientation: Orientation,
          enabled: Boolean = true,
          interactionSource: MutableInteractionSource? = null,
          startDragImmediately: Boolean = false,
          onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
          onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
          reverseDirection: Boolean = false
      )
      

      最主要的參數 state 需要可以記錄拖動狀態,獲取到拖動手勢的偏移量。orientation 指定監聽拖動的方向。
      使用示例:

      @Composable
      fun DraggableBox() {
          var offsetX by remember {
              mutableFloatStateOf(0f)
          }
          Box(modifier = Modifier
              .offset {
                  IntOffset(x = offsetX.roundToInt(), y = 0)
              }
              .background(Color.LightGray)
              .size(80.dp)
              .draggable(
                  orientation = Orientation.Horizontal,
                  state = rememberDraggableState { onDelta ->
                      offsetX += onDelta
                  }
              ))
      }
      

      注意:拖動手勢本身不會讓 UI 發生變化。通過 rememberDraggableState 構造一個 DraggableState,獲取拖動偏移量,然后把這個偏移量累加到某個狀態變量上,利用這個狀態來改變 UI 界面。
      比如這里使用了 offset 去改變組件的偏移量。

      注意:由于Modifer鏈式執行,此時offset必需在draggable與background前面。

      3.2 Modifier.draggable2D

      Modifier.draggable 通過 orientation 參數指定方向,只能水平或者垂直方向拖動。而 Modifier.draggable2D 則是可以同時沿著水平或者垂直方向拖動。用法如下:

      @OptIn(ExperimentalFoundationApi::class)
      @Composable
      fun Draggable2DBox() {
          var offset by remember {
              mutableStateOf(Offset.Zero)
          }
          Box(modifier = Modifier
              .offset {
                  IntOffset(x = offset.x.roundToInt(), y = offset.y.roundToInt())
              }
              .background(Color.LightGray)
              .size(80.dp)
              .draggable2D(
                  state = rememberDraggable2DState { onDelta ->
                      offset += onDelta
                  }
              ))
      }
      

      Modifier.draggable 相比,Modifier.draggable2D 修飾符沒有了 orientation 參數,無需指定方向,同時,state 類型是 Draggable2DState。構造該 State 的 lambda 表達式的參數,delta 類型也變成了 Offset 類型。這樣就實現了在 2D 平面上的任意方向拖動。

      四、錨定拖動

      Modifier.anchoredDraggable 是 Jetpack Compose 1.6.0 引入的一個新的修飾符,替代了 Swipeable, 用來實現錨定拖動。

      fun <T> Modifier.anchoredDraggable(
          state: AnchoredDraggableState<T>,
          orientation: Orientation,
          enabled: Boolean = true,
          reverseDirection: Boolean = false,
          interactionSource: MutableInteractionSource? = null
      )
      

      此修飾符參數:

      • state 一個 DraggableState 的實例。
      • orientation 我們要將內容拖入的方向,水平或者垂直。
      • enabled 用于啟用/禁用拖動手勢。
      • reverseDirection 是否反轉拖動方向。
      • interactionSource 用于拖動手勢的可選交互源。

      錨定拖動對象有 2 個主要部分,一個是應用于要拖動的內容的修飾符,另一個是其狀態 AnchoredDraggableState,它指定拖動的操作方式。

      除了構造函數之外,為了使用 anchoredDraggable 修飾符,我們還需要熟悉其他幾個 API,它們是 updateAnchors 和 requireOffset。

      4.1 可拖動狀態 AnchoredDraggableState

      class AnchoredDraggableState<T>(
          initialValue: T,
          internal val positionalThreshold: (totalDistance: Float) -> Float,
          internal val velocityThreshold: () -> Float,
          val animationSpec: AnimationSpec<Float>,
          internal val confirmValueChange: (newValue: T) -> Boolean = { true }
      )
      

      在這個構造函數中,我們有

      • initialValue,一個參數化參數,用于在首次呈現時捕捉可拖動的內容。
      • positionalThreshold 一個 lambda 表達式,用于根據錨點之間的距離確定內容是以動畫形式呈現到下一個錨點還是返回到原始錨點。
      • velocityTheshold 一個 lambda 表達式,它返回一個速度,用于確定我們是否應該對下一個錨點進行動畫處理,而不考慮位置Theshold。如果拖動速度超過此閾值,那么我們將對下一個錨點進行動畫處理,否則使用 positionalThreshold。
      • animationSpec,用于確定如何對可拖動內容進行動畫處理。
      • confirmValueChange 一個lambda 表達式,可選參數,可用于否決對可拖動內容的更改。

      值得注意的是,目前沒有可用的 rememberDraggableState 工廠方法,因此我們需要通過 remember 手動定義可組合文件中的狀態。

      4.2 updateAnchors

      fun updateAnchors(
          newAnchors: DraggableAnchors<T>,
          newTarget: T = if (!offset.isNaN()) {
              newAnchors.closestAnchor(offset) ?: targetValue
          } else targetValue
      )
      

      我們使用 updateAnchors 方法指定內容將捕捉到的拖動區域上的停止點。我們至少需要指定 2 個錨點,以便可以在這 2 個錨點之間拖動內容,但我們可以根據需要添加任意數量的錨點。

      4.3 requireOffset

      此方法僅返回可拖動內容的偏移量,以便我們可以將其應用于內容。同樣,anchoredDraggable 修飾符本身不會在拖動時移動內容,它只是計算用戶在屏幕上拖動時的偏移量,我們需要自己根據 requireOffset 提供的偏移量更新內容。

      4.4 使用示例介紹

      // 1. 定義錨點
      enum class DragAnchors {
          Start,
          End,
      }
      
      @OptIn(ExperimentalFoundationApi::class)
      @Composable
      fun DragAnchorDemo() {
          val density = LocalDensity.current
      
          // 2. 使用 remember 聲明 AnchoredDraggableState,確保重組過程中能夠緩存結果
          val state = remember {
              AnchoredDraggableState(
                  // 3. 設置 AnchoredDraggableState 的初始錨點值
                  initialValue = DragAnchors.Start,
      
                  // 4. 根據行進的距離確定我們是否對下一個錨點進行動畫處理。在這里,我們指定確定我們是否移動到下一個錨點的閾值是到下一個錨點距離的一半——如果我們移動了兩個錨點之間的半點,我們將對下一個錨點進行動畫處理,否則我們將返回到原點錨點。
                  positionalThreshold = { totalDistance ->
                      totalDistance * 0.5f
                  },
                  
                  // 5.確定將觸發拖動內容以動畫形式移動到下一個錨點的最小速度,而不管是否 已達到 positionalThreshold 指定的閾值。
                  velocityThreshold = {
                      with(density) {
                          100.dp.toPx()
                      }
                  },
      
                  // 6. 指定了在釋放拖動手勢時如何對下一個錨點進行動畫處理;這里我們使用 一個補間動畫,它默認為 FastOutSlowIn 插值器
                  animationSpec = tween(),
      
                  confirmValueChange = { newValue ->
                      true
                  }
              ).apply {
      
                  // 7. 使用前面介紹的 updateAnchors 方法定義內容的錨點 
                  updateAnchors(
      
                      // 8. 使用 DraggableAnchors 幫助程序方法指定要使用的錨點 。我們在這里所做的是創建 DragAnchors 到內容的實際偏移位置的映射。在這種情況下,當狀態為“開始”時,內容將偏移量為 0 像素,當狀態為“結束”時,內容偏移量將為 400 像素。
                      DraggableAnchors {
                          DragAnchors.Start at 0f
                          DragAnchors.End at 800f
                      }
                  )
              }
          }
      
          Box {
              Image(
                  painter = painterResource(id = R.mipmap.ic_test), contentDescription = null,
                  modifier = Modifier
                      .offset {
                          IntOffset(x = 0, y = state.requireOffset().roundToInt())
                      }
                      .clip(CircleShape)
                      .size(80.dp)
                      // 使用前面定義的狀態
                      .anchoredDraggable(
                          state = state,
                          orientation = Orientation.Vertical
                      )
              )
          }
      }
      

      注意: offset 要先于 anchoredDraggable 調用
      看看效果:

      拖動超多一半的距離,或者速度超過閾值,就會以動畫形式跳到下一個錨點。

      五、轉換手勢

      5.1 Modifier.transformer

      Modifier.transformer 修飾符允許開發者監聽 UI 組件的雙指拖動、縮放或旋轉手勢,通過所提供的信息來實現 UI 動畫效果。

      @ExperimentalFoundationApi
      fun Modifier.transformable(
          state: TransformableState,
          canPan: (Offset) -> Boolean,
          lockRotationOnZoomPan: Boolean = false,
          enabled: Boolean = true
      )
      
      • transformableState 必傳參數,可以使用 rememberTransformableState 創建一個 transformableState, 通過 rememberTransformableState 的尾部 lambda 可以獲取當前雙指拖動、縮放或旋轉手勢信息。

      • lockRotationOnZoomPan 可選參數,當主動設置為 true 時,當UI組件已發生雙指拖動或縮放時,將獲取不到旋轉角度偏移量信息。

      使用示例:

      @Composable
      fun TransformBox() {
          var offset by remember { mutableStateOf(Offset.Zero) }
          var rotationAngle by remember { mutableStateOf(0f) }
          var scale by remember { mutableStateOf(1f) }
      
          Box(modifier = Modifier
              .size(80.dp)
              .rotate(rotationAngle) // 需要注意 offset 與 rotate 的調用先后順序
              .offset {
                  IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
              }
              .scale(scale)
              .background(Color.LightGray)
              .transformable(
                  state = rememberTransformableState { zoomChange: Float, panChange: Offset, rotationChange: Float ->
                      scale *= zoomChange
                      offset += panChange
                      rotationAngle += rotationChange
                  }
              )
          )
      }
      

      注意:由于 Modifer 鏈式執行,此時需要注意 offset 與 rotate 調用的先后順序
      ??示例( offset 在 rotate 前面): 一般情況下我們都需要組件在旋轉后,當出現雙指拖動時組件會跟隨手指發生偏移。若 offset 在 rotate 之前調用,則會出現組件旋轉后,當雙指拖動時組件會以當前旋轉角度為基本坐標軸進行偏移。這是由于當你先進行 offset 說明已經發生了偏移,而 rotate 時會改變當前UI組件整個坐標軸,所以出現與預期不符的情況出現。

      效果如下:

      六、自定義觸摸反饋

      6.1 Modifier.pointerInput

      前面已經介紹完常用的手勢處理了,都非常簡單。但是有時候我們需要自定義觸摸反饋。這時候可以就需要使用到 Modifier.PointerInput 修飾符了。該修飾符提供了更加底層細粒度的手勢檢測,前面講到的高級別修飾符實際上最終都是用底層低級別 API 來實現的。

      fun Modifier.pointerInput(
          vararg keys: Any?,
          block: suspend PointerInputScope.() -> Unit
      )
      

      看下參數:

      • keys 當 Composable 組件發生重組時,如果傳入的 keys 發生了變化,則手勢事件處理過程會被中斷。
      • block 在這個 PointerInputScope 類型作用域代碼塊中我們便可以聲明手勢事件處理邏輯了。通過 suspend 關鍵字可知這是個協程體,這意味著在 Compose 中手勢處理最終都發生在協程中。

      在 PointerInputScope 作用域內,可以使用更加底層的手勢檢測的基礎API。

      6.1.1 點擊類型的基礎 API

      API名稱 作用
      detectTapGestures 監聽點擊手勢
      suspend fun PointerInputScope.detectTapGestures(
        onDoubleTap: ((Offset) -> Unit)? = null,
        onLongPress: ((Offset) -> Unit)? = null,
        onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
        onTap: ((Offset) -> Unit)? = null
      )
      

      看一下這幾個方法名,就能知道方法的作用。使用起來與前面講解高級的修飾符差不多。在 PointerInputScope 中使用 detectTapGestures,不會帶有漣波紋效果,方便我們根據需要進行定制。

      • onDoubleTap (可選):雙擊時回調
      • onLongPress (可選):長按時回調
      • onPress (可選):按下時回調
      • onTap (可選):輕觸時回調

      這幾種點擊事件回調存在著先后次序的,并不是每次只會執行其中一個。onPress 是最普通的 ACTION_DOWN 事件,你的手指一旦按下便會回調。如果你連著按了兩下,則會在執行兩次 onPress 后執行 onDoubleTap。如果你的手指按下后不抬起,當達到長按的判定閾值 (400ms) 會執行 onLongPress。如果你的手指按下后快速抬起,在輕觸的判定閾值內(100ms)會執行 onTap 回調。

      總的來說, onDoubleTap 回調前必定會先回調 2 次 Press,而 onLongPress 與 onTap 回調前必定會回調 1 次 Press

      使用如下:

      @Composable
      fun PointerInputDemo() {
          Box(modifier = Modifier.background(Color.LightGray).size(100.dp)
              .pointerInput(Unit) {
                  detectTapGestures(
                      onDoubleTap = {
                          Log.i("sharpcj", "onDoubleTap --> $it")
                      },
                      onLongPress = {
                          Log.i("sharpcj", "onLongPress --> $it")
                      },
                      onPress = {
                          Log.i("sharpcj", "onPress --> $it")
                      },
                      onTap = {
                          Log.i("sharpcj", "onTap --> $it")
                      }
                  )
              }
          )
      }
          
      

      6.1.2 拖動類型基礎 API

      API名稱 作用
      detectDragGestures 監聽拖動手勢
      detectDragGesturesAfterLongPress 監聽長按后的拖動手勢
      detectHorizontalDragGestures 監聽水平拖動手勢
      detectVerticalDragGestures 監聽垂直拖動手勢

      detectDragGesturesAfterLongPress 為例:

      @Composable
      fun PointerInputDemo() {
          Box(modifier = Modifier.background(Color.LightGray).size(100.dp)
              .pointerInput(Unit) {
                  detectDragGesturesAfterLongPress(
                      onDragStart = {
      
                      },
                      onDrag = { change: PointerInputChange, dragAmount: Offset ->
      
                      },
                      onDragEnd = {
      
                      },
                      onDragCancel = {
      
                      }
                  )
              }
          )
      }
      

      該 API 會檢測長按后的拖動,提供了四個回調時機,onDragStart 會在拖動開始時回調,onDragEnd 會在拖動結束時回調,onDragCancel 會在拖動取消時回調,而 onDrag 則會在拖動真正發生時回調。

      注意:

      1. onDragCancel 觸發時機多發生于滑動沖突的場景,子組件可能最開始是可以獲取到拖動事件的,當拖動手勢事件達到莫個指定條件時可能會被父組件劫持消費,這種場景下便會執行 onDragCancel 回調。所以 onDragCancel 回調主要依賴于實際業務邏輯。
      2. 上述 API 會檢測長按后的拖動,但是其本身并沒有提供長按時的回調方法。如果要同時監聽長按,可以配合 detectTapGestures 一起使用。

      由于這些檢測器是頂級檢測器,因此無法在一個 pointerInput 修飾符中添加多個檢測器。以下代碼段只會檢測點按操作,而不會檢測拖動操作。

      var log by remember { mutableStateOf("") }
      Column {
          Text(log)
          Box(
              Modifier
                  .size(100.dp)
                  .background(Color.Red)
                  .pointerInput(Unit) {
                      detectTapGestures { log = "Tap!" }
                      // Never reached
                      detectDragGestures { _, _ -> log = "Dragging" }
                  }
          )
      }
      

      在內部,detectTapGestures 方法會阻塞協程,并且永遠不會到達第二個檢測器。如果需要向可組合項添加多個手勢監聽器,可以改用單獨的 pointerInput 修飾符實例:

      var log by remember { mutableStateOf("") }
      Column {
          Text(log)
          Box(
              Modifier
                  .size(100.dp)
                  .background(Color.Red)
                  .pointerInput(Unit) {
                      detectTapGestures { log = "Tap!" }
                  }
                  .pointerInput(Unit) {
                      // These drag events will correctly be triggered
                      detectDragGestures { _, _ -> log = "Dragging" }
                  }
          )
      }
      

      6.1.3 轉換類型基礎 API

      API名稱 作用
      detectTransformGestures 監聽拖動、縮放與旋轉手勢
      @Composable
      fun PointerInputDemo() {
          Box(modifier = Modifier.background(Color.LightGray).size(100.dp)
              .pointerInput(Unit) {
                      detectTransformGestures(
                          panZoomLock = false,
                          onGesture = { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
      
                          }
                      )
                  }
              )
      }
      

      Modifier.transfomer 修飾符不同的是,通過這個 API 可以監聽單指的拖動手勢,和拖動類型基礎API所提供的功能一樣,除此之外還支持監聽雙指縮放與旋轉手勢。反觀 Modifier.transfomer 修飾符只能監聽到雙指拖動手勢。

      • panZoomLock(可選): 當拖動或縮放手勢發生時是否支持旋轉
      • onGesture(必須):當拖動、縮放或旋轉手勢發生時回調

      6.2 awaitPointerEventScope

      前面介紹的 GestureDetector 系列 API 本質上仍然是一種封裝,既然手勢處理是在協程中完成的,所以手勢監聽必然是通過協程的掛起恢復實現的,以取代傳統的回調監聽方式。

      PointerInputScope 中我們使用 awaitPointerEventScope 方法獲得 AwaitPointerEventScope 作用域,在 AwaitPointerEventScope 作用域中我們可以使用 Compose 中所有低級別的手勢處理掛起方法。當 awaitPointerEventScope 內所有手勢事件都處理完成后 awaitPointerEventScope 便會恢復執行將 Lambda 中最后一行表達式的數值作為返回值返回。

      suspend fun <R> awaitPointerEventScope(
          block: suspend AwaitPointerEventScope.() -> R
      ): R
      

      AwaitPointerEventScope 中提供了一些基礎手勢方法:

      API名稱 作用
      awaitPointerEvent 手勢事件
      awaitFirstDown 第一根手指的按下事件
      drag 拖動事件
      horizontalDrag 水平拖動事件
      verticalDrag 垂直拖動事件
      awaitDragOrCancellation 單次拖動事件
      awaitHorizontalDragOrCancellation 單次水平拖動事件
      awaitVerticalDragOrCancellation 單次垂直拖動事件
      awaitTouchSlopOrCancellation 有效拖動事件
      awaitHorizontalTouchSlopOrCancellation 有效水平拖動事件
      awaitVerticalTouchSlopOrCancellation 有效垂直拖動事件

      6.2.1 原始時間 awaitPointerEvent

      上層所有手勢監聽 API 都是基于這個 API 實現的,他的作用類似于傳統 View 中的 onTouchEvent() 。無論用戶是按下、移動或抬起都將視作一次手勢事件,當手勢事件發生時 awaitPointerEvent 便會恢復返回監聽到的屏幕上所有手指的交互信息。

      以下代碼可以用來監聽原始的指針事件。

      @Composable
      fun PointerEventDemo() {
          Box(modifier = Modifier
              .background(Color.LightGray)
              .size(100.dp)
              .pointerInput(Unit) {
                  awaitPointerEventScope {
                      while (true) {
                          val event = awaitPointerEvent()
                          Log.d(
                              "sharpcj",
                              "event --> type: ${event.type} - x: ${event.changes[0].position.x} - y: ${event.changes[0].position.y}"
                          )
                      }
                  }
              })
      }
      

      我們點擊,看到日志如下:

      D  event --> type: Press - x: 188.0 - y: 124.0
      D  event --> type: Release - x: 188.0 - y: 124.0
      

      我們可以看到事件的 type 為 PressRelease

      點擊移動,日志如下:

      D  event --> type: Press - x: 178.0 - y: 178.0
      D  event --> type: Move - x: 181.93164 - y: 175.06836
      D  event --> type: Move - x: 183.99316 - y: 174.0
      D  event --> type: Move - x: 185.5 - y: 171.0
      D  event --> type: Move - x: 191.0 - y: 164.0
      D  event --> type: Release - x: 191.0 - y: 164.0
      

      注意事件的 type 為 PressMoveRelease

      • awaitPointerEventScope 創建可用于等待指針事件的協程作用域
      • awaitPointerEvent 會掛起協程,直到發生下一個指針事件

      以上監聽原始輸入事件非常強大,類似于傳統 View 中完全實現 onTouchEvent 方法。但是也很復雜。實際場景中幾乎不會使用,而是直接使用前面講到的手勢檢測 GestureDetect API。

      6.3 awaitEachGesture

      Compose 手勢操作實際上是在協程中監聽處理的,當協程處理完一輪手勢交互后便會結束,當進行第二次手勢交互時由于負責手勢監聽的協程已經結束,手勢事件便會被丟棄掉。為了讓手勢監聽協程能夠不斷地處理每一輪的手勢交互,很容易想到可以在外層嵌套一個 while(true) 進行實現,然而這么做并不優雅,且也存在著一些問題。更好的處理方式是使用 awaitEachGesture, awaitEachGesture 方法保證了每一輪手勢處理邏輯的一致性。實際上前面所介紹的 GestureDetect 系列 API,其內部實現都使用了 forEachGesture。

      suspend fun PointerInputScope.awaitEachGesture(block: suspend AwaitPointerEventScope.() -> Unit) {
          val currentContext = currentCoroutineContext()
          awaitPointerEventScope {
              while (currentContext.isActive) {
                  try {
                      block()
      
                      // Wait for all pointers to be up. Gestures start when a finger goes down.
                      awaitAllPointersUp()
                  } catch (e: CancellationException) {
                      if (currentContext.isActive) {
                          // The current gesture was canceled. Wait for all fingers to be "up" before
                          // looping again.
                          awaitAllPointersUp()
                      } else {
                          // detectGesture was cancelled externally. Rethrow the cancellation exception to
                          // propagate it upwards.
                          throw e
                      }
                  }
              }
          }
      }
      

      在 awaitEachGesture 中使用特定的手勢事件

      @Composable
      fun PointerEventDemo() {
          Box(modifier = Modifier
              .background(Color.LightGray)
              .size(100.dp)
              .pointerInput(Unit) {
                  awaitEachGesture {
                      awaitFirstDown().also { 
                          it.consume()
                          Log.d("sharpcj", "down")
                      }
                      val up = waitForUpOrCancellation()
                      if (up != null) {
                          up.consume()
                          Log.d("sharpcj", "up")
                      }
                  }
              }
          )
      }
      

      6.3 多指事件

      awaitPointerEvent 返回一個 PointerEvent, PointerEvent 中包含一個集合val changes: List<PointerInputChange> 這里面包含了所有手指的事件信息,我們看看 PointerInputChange

      @Immutable
      class PointerInputChange(
          val id: PointerId,
          val uptimeMillis: Long,
          val position: Offset,
          val pressed: Boolean,
          val pressure: Float,
          val previousUptimeMillis: Long,
          val previousPosition: Offset,
          val previousPressed: Boolean,
          isInitiallyConsumed: Boolean,
          val type: PointerType = PointerType.Touch,
          val scrollDelta: Offset = Offset.Zero
      ) 
      

      PointerInputChange 包含某個手指的事件具體信息。
      比如前面打印日志的時候,使用了 event.changes[0].position 獲取坐標信息。

      6.4 事件分發

      6.4.1 事件調度

      并非所有指針事件都會發送到每個 pointerInput 修飾符。事件分派的工作原理如下:

      • 系統會將指針事件分派給可組合層次結構。新指針觸發其第一個指針事件時,系統會開始對“符合條件”的可組合項進行命中測試。如果可組合項具有指針輸入處理功能,則會被視為符合條件。命中測試從界面樹頂部流向底部。當指針事件發生在可組合項的邊界內時,即被視為“命中”。此過程會產生一個“命中測試正例”的可組合項鏈。
      • 默認情況下,當樹的同一級別上有多個符合條件的可組合項時,只有 Z-index 最高的可組合項才是“hit”。例如,當您向 Box 添加兩個重疊的 Button 可組合項時,只有頂部繪制的可組合項才會收到任何指針事件。從理論上講,您可以通過創建自己的 PointerInputModifierNode 實現并將 sharePointerInputWithSiblings 設為 true 來替換此行為。
      • 系統會將同一指針的其他事件分派到同一可組合項鏈,并根據事件傳播邏輯流動。系統不再對此指針執行命中測試。這意味著鏈中的每個可組合項都會接收該指針的所有事件,即使這些事件發生在該可組合項的邊界之外時。不在鏈中的可組合項永遠不會收到指針事件,即使指針位于其邊界內也是如此。
        由鼠標或觸控筆懸停時觸發的懸停事件不屬于此處定義的規則。懸停事件會發送給用戶點擊的任意可組合項。因此,當用戶將指針從一個可組合項的邊界懸停在下一個可組合項的邊界上時,事件會發送到新的可組合項,而不是將事件發送到第一個可組合項。

      官方文檔的描述比較清楚,為了更加直觀,還是自己寫示例說明一下:

      @Composable
      fun EventConsumeDemo() {
          Box(modifier = Modifier
              .background(Color.LightGray)
              .size(300.dp)
              .pointerInput(Unit) {
                  awaitEachGesture {
                      while (true) {
                          val event = awaitPointerEvent()
                          Log.d("sharpcj", "outer box --> ${event.type}")
                      }
                  }
              }) {
              Box(modifier = Modifier
                  .background(Color.Yellow)
                  .size(200.dp)
                  .pointerInput(Unit) {
                      awaitEachGesture {
                          while (true) {
                              val event = awaitPointerEvent()
                              Log.d("sharpcj", "inner box --> ${event.type}")
                          }
                      }
                  })
          }
      }
      

      如上代碼,我們在 inner Box 中輕畫一下。日志如下:

      D  inner box --> Press
      D  out box --> Press
      D  inner box --> Move
      D  out box --> Move
      D  inner box --> Move
      D  out box --> Move
      D  inner box --> Release
      D  out box --> Release
      

      解釋:

      1. inner Box 和 outer Box 都會收到事件, 因為點擊的位置同時處在 inner Box 和 outer Box 之中,
      2. 由于 inner Box 的 Z-index 更高,所以先收到事件。

      6.4.2 事件消耗

      如果為多個可組合項分配了手勢處理程序,這些處理程序不應沖突。例如,我們來看看以下界面:

      當用戶點按書簽按鈕時,該按鈕的 onClick lambda 會處理該手勢。當用戶點按列表項的任何其他部分時,ListItem 會處理該手勢并轉到文章。就指針輸入而言,Button 必須“消費”此事件,以便其父級知道不會再對其做出響應。開箱組件中包含的手勢和常見的手勢修飾符就包含這種使用行為,但如果您要編寫自己的自定義手勢,則必須手動使用事件。可以使用 PointerInputChange.consume 方法執行此操作:

      Modifier.pointerInput(Unit) {
      
          awaitEachGesture {
              while (true) {
                  val event = awaitPointerEvent()
                  // consume all changes
                  event.changes.forEach { it.consume() }
              }
          }
      }
      

      使用事件不會阻止事件傳播到其他可組合項。可組合項需要明確忽略已使用的事件。編寫自定義手勢時,您應檢查某個事件是否已被其他元素使用:

      Modifier.pointerInput(Unit) {
          awaitEachGesture {
              while (true) {
                  val event = awaitPointerEvent()
                  if (event.changes.any { it.isConsumed }) {
                      // A pointer is consumed by another gesture handler
                  } else {
                      // Handle unconsumed event
                  }
              }
          }
      }
      

      看實際場景,當有兩個組件疊加起來的時候,我們更多時候只是希望外層的組件響應事件。怎么處理,還是看上面的例子,我們只希望 inner Box 處理事件。修改代碼如下:

      @Composable
      fun EventConsumeDemo() {
          Box(modifier = Modifier
              .background(Color.LightGray)
              .size(300.dp)
              .pointerInput(Unit) {
                  awaitEachGesture {
                      while (true) {
                          val event = awaitPointerEvent()
                          if (event.changes.any{ it.isConsumed }) {
                              Log.d("sharpcj", "A pointer is consumed by another gesture handler")
                          } else {
                              Log.d("sharpcj", "out box --> ${event.type}")
                          }
                      }
                  }
              }) {
              Box(modifier = Modifier
                  .background(Color.Yellow)
                  .size(200.dp)
                  .pointerInput(Unit) {
                      awaitEachGesture {
                          while (true) {
                              val event = awaitPointerEvent()
                              Log.d("sharpcj", "inner box --> ${event.type}")
                              event.changes.forEach{
                                  it.consume()
                              }
                          }
                      }
                  })
          }
      }
      

      結果:

      D  inner box --> Press
      D  A pointer is consumed by another gesture handler
      D  inner box --> Move
      D  A pointer is consumed by another gesture handler
      D  inner box --> Move
      D  A pointer is consumed by another gesture handler
      D  inner box --> Move
      D  A pointer is consumed by another gesture handler
      D  inner box --> Release
      D  A pointer is consumed by another gesture handler
      

      解釋:

      1. 我們在 inner Box 先收到事件并且處理之后,調用 event.changes.forEach { it.consume() } 將所有的事件都消費掉。
      2. inner Box 將事件消費掉,并不能阻止 outer Box 收到事件。
      3. 需要在 outer Box 中通過判斷事件是否被消費,來編寫正確的邏輯處理。

      再修改下代碼,我們使用上層的 GestureDetect API ,再次測試:

      @Composable
      fun EventConsumeDemo2() {
          Box(modifier = Modifier
              .background(Color.LightGray)
              .size(300.dp)
              .pointerInput(Unit) {
                  detectTapGestures(
                      onTap = {
                          Log.d("sharpcj", "outer Box onTap")
                      }
                  )
              }
          ) {
              Box(modifier = Modifier
                  .background(Color.Yellow)
                  .size(200.dp)
                  .pointerInput(Unit) {
                      detectTapGestures(
                          onTap = {
                              Log.d("sharpcj", "inner Box onTap")
                          }
                      )
                  })
          }
      }
      

      結果如下:

      D  inner Box onTap
      D  inner Box onTap
      D  inner Box onTap
      D  inner Box onTap
      

      解釋:
      Jetpack Compose 提供的開箱組件中包含的手勢和常見的手勢修飾符默認就做了上述判斷,事件只能被 Z-Index 最高的組件處理。

      6.4.3 事件傳播

      如前所述,指針事件會傳遞到其命中的每個可組合項。當有多個可組合項“疊”在一起的時候,事件會按什么順序傳播呢?
      實際上,事件會有三次流經可組合項:

      • Initial 在初始傳遞中,事件從界面樹頂部流向底部。此流程允許父項在子項使用事件之前攔截事件。
      • Main 在主傳遞中,事件從界面樹的葉節點一直流向界面樹的根。此階段是您通常使用手勢的位置,也是監聽事件時的默認傳遞。處理此傳遞中的手勢意味著葉節點優先于其父節點,這是大多數手勢最符合邏輯的行為。在此示例中,Button 會在 ListItem 之前收到事件。
      • Final 在“最終通過”中,事件會再一次從界面樹頂部流向葉節點。此流程允許堆棧中較高位置的元素響應其父項的事件消耗。例如,當按下按鈕變為可滾動父項的拖動時,按鈕會移除其漣漪指示。

      實際上在諸如 awaitPointerEvent 的方法中,有一個參數 PointerEventPass,用來控制事件傳播的。

      suspend fun awaitPointerEvent(
          pass: PointerEventPass = PointerEventPass.Main
      )
      

      分發順序:

      PointerEventPass.Initial -> PointerEventPass.Main -> PointerEventPass.Final
      

      對應了上面的描述。
      看示例:

      @Composable
      fun EventConsumeDemo() {
          Box(modifier = Modifier
              .background(Color.LightGray)
              .size(300.dp)
              .pointerInput(Unit) {
                  awaitEachGesture {
                      while (true) {
                          val event = awaitPointerEvent(PointerEventPass.Main)
                          Log.d("sharpcj", "box1 --> ${event.type}")
                      }
                  }
              }) {
              Box(modifier = Modifier
                  .background(Color.Yellow)
                  .size(250.dp)
                  .pointerInput(Unit) {
                      awaitEachGesture {
                          while (true) {
                              val event = awaitPointerEvent(PointerEventPass.Initial)
                              Log.d("sharpcj", "box2 --> ${event.type}")
                          }
                      }
                  }) {
                  Box(modifier = Modifier
                      .background(Color.Blue)
                      .size(200.dp)
                      .pointerInput(Unit) {
                          awaitEachGesture {
                              while (true) {
                                  val event = awaitPointerEvent(PointerEventPass.Final)
                                  Log.d("sharpcj", "box3 --> ${event.type}")
                              }
                          }
                      }) {
                      Box(modifier = Modifier
                          .background(Color.Red)
                          .size(150.dp)
                          .pointerInput(Unit) {
                              awaitEachGesture {
                                  while (true) {
                                      val event = awaitPointerEvent()
                                      Log.d("sharpcj", "box4 --> ${event.type}")
                                  }
                              }
                          })
                  }
              }
          }
      }
      

      運行結果:

      D  box2 --> Press
      D  box4 --> Press
      D  box1 --> Press
      D  box3 --> Press
      D  box2 --> Move
      D  box4 --> Move
      D  box1 --> Move
      D  box3 --> Move
      D  box2 --> Move
      D  box4 --> Move
      D  box1 --> Move
      D  box3 --> Move
      D  box2 --> Release
      D  box4 --> Release
      D  box1 --> Release
      D  box3 --> Release
      

      解釋:

      1. Initial 傳遞由根節點到葉子結點依次傳遞,其中 Box2 攔截了。所有 Box2 優先處理事件。
      2. Main 傳遞由葉子節點傳遞到父節點, Box1 顯示聲明了 PointerEventPass.Main 和 Box4 沒有聲明,但是默認參數也是 PointerEventPass.Main, 由于是從葉子結點向根節點傳播,所以 Box4 先收到事件,然后是 Box1 收到事件。
      3. Final 事件再次從根節點傳遞到葉子結點,這里只有 Box3 參數是 PointerEventPass.Final,所以 Box3 最后收到事件。

      以上是事件傳播的分析,關于消費,同理,如果先收到事件的可組合項把事件消費了,后收到事件的組件根據需要判斷事件是否被消費即可。

      七、嵌套滾動 Modifier.NestedScroll

      關于嵌套滾動,相對復雜一點。不過在 Compose 中,使用 Modifier.NestedScroll 修飾符來實現,也不難學。
      下一篇文章單獨來介紹。

      posted @ 2024-06-27 20:41  SharpCJ  閱讀(2427)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产999久久高清免费观看| 蜜臀91精品国产高清在线| 小13箩利洗澡无码视频网站| 国产一区精品在线免费看| 伊人久久大香线蕉av五月天| 国产在线无码精品无码| 安西县| 久久精品国产一区二区三区| 国产一区二区日韩经典| 亚洲精品漫画一二三区| 成全影视大全在线观看| 午夜福利在线观看入口| 亚洲av一本二本三本| 人妻放荡乱h文| 免费超爽大片黄| 亚洲熟女乱综合一区二区| 性做久久久久久久| 国产精品国产三级国快看| 国产三级精品三级在线观看| 亚洲老妇女一区二区三区| 豆国产97在线 | 亚洲| 俄罗斯老熟妇性爽xxxx| 久热这里只有精品12| 国产精品无码a∨麻豆| 亚洲一区二区三区人妻天堂 | 亚洲欧美日韩在线码| 国产乱码1卡二卡3卡四卡5 | 久久精品女人的天堂av| 亚洲热无码av一区二区东京热av | 亚洲精品国产电影| 四房播色综合久久婷婷| 亚洲夜夜欢一区二区三区| 四虎成人精品无码永久在线| 亚洲美女厕所偷拍美女尿尿| 插插无码视频大全不卡网站| 久久99精品久久久久久9| 国产欧美久久一区二区| 亚洲午夜福利精品无码不卡| 厨房与子乱在线观看| 亚洲国产在一区二区三区| 亚洲爆乳精品无码一区二区|