In this article, we’ll look at Compose’s gestures. Compose provides a number of direct apis that make it easy to detect a user’s gestures. We’ll go through them one by one.

A: Modifier. Clickable

Since Clickable we have been using the example of a counting Text directly on the official website. Click the number +1 and the code looks like this:

@Composable
fun ClickableSample(a) {
    val count = remember { mutableStateOf(0)}// content that you want to make clickable
    Text(
        text = count.value.toString(),
        modifier = Modifier.clickable { count.value += 1})}Copy the code

The effect is as follows:

2: Modifier. VerticalScroll with Modifier. HorizontalScroll (perform rolling)

VerticalScroll and horizontalScroll when the content of the control is larger than the size of the control itself. Use these two modifiers to scroll. The code for these two methods is as follows:

fun Modifier.verticalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
)= {... }fun Modifier.horizontalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
){... }Copy the code
  • State is the scrolling state
  • Enabled Whether it is available
  • flingBehavior
  • ReverseScrolling whether to roll backwards

For example: when the list is scrolling to the top, we click the scroll button to scroll down, and when the list is scrolling to the bottom, we click the button to scroll up.

@Preview
@Composable
private fun ScrollBoxesSmooth(a) {

    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    val scope = rememberCoroutineScope()
    val isScrollBottom = remember {
        mutableStateOf(false)
    }

    Row() {
        Column(
            modifier = Modifier
                .background(Color.LightGray)
                .size(100.dp)
                .padding(horizontal = 8.dp)
                .verticalScroll(state)
        ) {
            repeat(10) {
                Text("Item $it", modifier = Modifier.padding(2.dp))
            }
        }

        Button(modifier = Modifier.padding(10.dp),onClick = {
            scope.launch {
                // When scrolling to the top, click to scroll down
                if(state.value<=0){
                    isScrollBottom.value = false
                }else if(state.value>=state.maxValue){
                    // When scrolling to the bottom, click to scroll up
                    isScrollBottom.value = true
                }
                state.animateScrollBy(if(isScrollBottom.value) -50f else 50f)
            }
        }) {
            Text(text = "Rolling")}}}Copy the code

Three: Modifier. Scrollable (monitor scrolling)

The difference between the Scrollable modifier and the verticalScroll and the horizontalScroll above is that the verticalScroll and the horizontalScroll are going to scroll, scroll the content. Scrollable, however, simply detects that the listener is scrolling. But scrollable doesn’t have any actual offset effects. Let’s look at the code for scrollable:

fun Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    flingBehavior: FlingBehavior? = null,
    interactionSource: MutableInteractionSource? = null
){... }Copy the code
  • State The rolling state ScrollableState is implemented through rememberScrollableState
  • Orientation.Vertical is a Vertical direction, while Horizontal is a Horizontal direction
  • Enabled Whether it is available
  • ReverseDirection Indicates whether scrolling is reversed
  • flingBehavior
  • The interactionSource can get the user’s state, such as whether it’s pressed and whether it’s focused. We talked about this before when I introduced Button. The interpretation of the Button

For example: We use a Text to display the value of our gesture scroll as follows:

@Preview
@Composable
fun scrollableTest(a){
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                state = rememberScrollableState {
                    offset+=it
                    it
                },
                orientation = Orientation.Vertical
            )
            .background(Color.LightGray),contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}
Copy the code

The effect is as follows:

Four: Modifier. NestedScroll nested slide

Compose automatically implements nested sliding, similar to the NestedScrollView for the native View. One is the need to customize the child control and parent control of the scroll logic (similar to View system NestedScrollingChild and NestedScrollingParent).

4.1 Automatic nesting slide

@Preview
@Composable
fun nestedScrollTest(a){
    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)
                    )
                }
            }
        }
    }
}
Copy the code

The effect is as follows:

4.2 Custom nested sliding

So let’s look at the code for nestedScroll

fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
){... }Copy the code
  • Connection NestedScrollConnection This class can receive events when a child View can be scrolled and provides some opportunities for us to use. Let’s look at this class in detail. NestedScrollConnection has the following methods.
    • fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = offset. Zero calls to dispatchPreScroll by the child control call back to this method, allowing the parent control to consume some drag events ahead of time. Parameters: available- The increment available for pre-scrolling, source – the source of the scrolling event. The return value is: the amount consumed.
    • fun onPostScroll( consumed: Offset,available: Offset, source: NestedScrollSource): Offset = Offset.Zero when the child control has exhausted its scroll, call dispatchPostScroll to tell the parent control that the child control is unable to scroll. The parent control can decide whether to continue processing the scroll. Consumed — the amount consumed by all nested scroll nodes under the hierarchy, available — the delta available for this connection, source — the source of the scroll. The return value is: the amount consumed
    • suspend fun onPreFling(available: Velocity): Zero When a quick scroll is performed, the child controls call dispatchPreFling, which is called back to onPreFling, allowing the parent control to intercept a portion of the fast scroll in advance.
    • suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {return velvelocity.Zero} When the child control finishes consuming fast slides, the method is called back, and the parent control can choose whether to continue consuming fast slides. Consumed: The speed consumed by the child View. Available: The speed at which the parent object is dropped after the child object. Returns the amount of speed consumed by the fast scroll operation
  • Dispatcher NestedScrollDispatcher is a distribution class for nested scrolling. There are several main methods
    • DispatchPreScroll notifies the parent control whether some events are consumed ahead of time. When the child control calls this method, NestedScrollConnection receives the onPreScroll callback. The code is as follows:
      fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
          returnparent? .onPreScroll(available, source) ? : Offset.Zero }Copy the code
    • DispatchPostScroll tells the parent control that my child control has run out of slides. When the child control calls this method, NestedScrollConnection receives the onPostScroll callback. The code is as follows:
      fun dispatchPostScroll(consumed: Offset,available: Offset, source: NestedScrollSource): Offset {
          returnparent? .onPostScroll(consumed, available, source) ? : Offset.Zero }Copy the code
    • DispatchPreFling notifies the parent control whether some quick slide events are consumed early. When the child control calls this method, the dispatchPreFling callback is received by NestedScrollConnection. The code is as follows:
      suspend fun dispatchPreFling(available: Velocity): Velocity {
          returnparent? .onPreFling(available) ? : Velocity.Zero }Copy the code
    • DispatchPostFling tells the parent control that the child control is running out of time. When the child control calls this method, the onPostFling callback is received by NestedScrollConnection. The code is as follows:
      suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {
          returnparent? .onPostFling(consumed, available) ? : Velocity.Zero }Copy the code

For nested slides, we can take a look at the following two flow diagrams to help us understand them more intuitively. One is Android. View system nested sliding. Thanks to Fu Chenming for the picture

The other one is for Compose.

For example, there is a title bar with an image in the middle and a toggle TAB bar. At the bottom is a list. When sliding, the image follows the TAB bar up to the top. TAB column top effect. The code is as follows:

@Preview
@Composable
fun nestedScrollTest(a){
    val imageHeight = 150.dp
    val headerHeight = 200.dp
    val topBarHeight = 48.dp
    val state = rememberLazyListState()
    val headerOffsetHeightPx = remember {
        mutableStateOf(0f)}val headerHeightPx = with(LocalDensity.current){
        imageHeight.roundToPx().toFloat()
    }
    val nestedScrollConnection = remember {
        object :NestedScrollConnection{
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // If the list has been scrolled down, you need to check whether it has been scrolled down
                if(available.y>0) {if(state.firstVisibleItemIndex<=2) {val delta = available.y
                        val newOffset = headerOffsetHeightPx.value + delta
                        headerOffsetHeightPx.value = newOffset.coerceIn(-headerHeightPx, 0f)}}else{
                    val delta = available.y
                    val newOffset = headerOffsetHeightPx.value + delta
                    headerOffsetHeightPx.value = newOffset.coerceIn(-headerHeightPx, 0f)}return Offset.Zero
            }
        }
    }
    val nestedScrollDispatcher = remember { NestedScrollDispatcher() }

    Box(modifier = Modifier.nestedScroll(nestedScrollConnection,nestedScrollDispatcher)){
        LazyColumn(state=state,contentPadding = PaddingValues(top = headerHeight+topBarHeight)) {
            items(100) { index ->
                Text("I'm item $index", modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp))
            }
        }
        headerView(headerOffsetHeightPx.value.roundToInt())
        TopAppBar(modifier = Modifier
            .height(48.dp)
            .background(Color.Blue),contentPadding = PaddingValues(start = 20.dp)) {
                Text(text = "Title",fontSize = 17.sp)
        }
    }
}

@Composable
fun headerView(headerOffsetY:Int){
    Column(modifier = Modifier.padding(top = 48.dp).offset {
        IntOffset(x = 0, y = headerOffsetY)
    }) {
        // A picture. At a height of 150 dp
        Image(modifier = Modifier
            .fillMaxWidth()
            .size(150.dp),bitmap = ImageBitmap.imageResource(id = R.drawable.icon_head), contentDescription = "Image",contentScale = ContentScale.FillBounds)
        // a TabRow with a height of 50dp
        tabRowView()
    }
}

@Composable
fun tabRowView(a){
    val tabIndex = remember {
        mutableStateOf(0)}val tabDatas = ArrayList<String>().apply {
        add("Chinese")
        add("Mathematics")
        add("English")
    }
    TabRow(
        selectedTabIndex = tabIndex.value,
        modifier = Modifier
            .fillMaxWidth()
            .height(50.dp),
        backgroundColor = Color.Green,
        contentColor = Color.Black,
        divider = {
            TabRowDefaults.Divider()
        },
        indicator = {
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(it[tabIndex.value]),
                color = Color.Blue,
                height = 2.dp
            )
        }
    ) {
        tabDatas.forEachIndexed{
                index, s ->
            tabView(index,s,tabIndex)
        }
    }
}

@Composable
fun tabView(index:Int,text:String,tabIndex:MutableState<Int>){
    val interactionSource = remember {
        MutableInteractionSource()
    }
    val isPress = interactionSource.collectIsPressedAsState().value
    Tab(
        selected = index == tabIndex.value,
        onClick = {
            tabIndex.value = index
        },
        modifier = Modifier
            .wrapContentWidth()
            .fillMaxHeight(),
        enabled =true,
        interactionSource = interactionSource,
        selectedContentColor = Color.Red,
        unselectedContentColor = Color.Black
    ) {
        Text(text = text,color = if(isPress || index == tabIndex.value) Color.Red else Color.Black)
    }
}
Copy the code

The renderings are as follows:

Five: Modifier. Draggable

A draggable listens for drag events, just like a scrollable. There is no actual modification to the offset value. Let’s look at the code for 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
){... }Copy the code
  • State Drag state DraggableState created by rememberDraggableState
  • Orientation orientation.Vertical, Horizontal
  • Enabled Whether it is available
  • The interactionSource can get the user’s state, such as whether it’s pressed and whether it’s focused. We talked about this before when I introduced Button. [Button’s Explanation]
  • When startDragImmediately is set to true, the DragTable will immediately start dragging and prevent other gesture detectors from reacting to “down” events (to prevent composite keystroke-based gestures)
  • OnDragStarted The callback to start dragging
  • OnDragStopped Stops the drag callback
  • ReverseDirection Specifies whether the direction is reversed

Example: Text drag, and by monitoring the distance of the drag to change the horizontal direction of the Offset Text, so as to achieve the effect of dragging. The code is as follows:

@Preview
@Composable
fun draggableTest(a){
    var offsetX by remember { mutableStateOf(0f) }
    Text(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState { delta ->
                    offsetX += delta
                },
                onDragStarted = {
                    Log.e("ccm"."startDrag")
                },
                onDragStopped = {
                    Log.e("ccm"."endDrag")
                }
            ),
        text = "Drag me!")}Copy the code

Six: Modifier. Swipeable

Using the swipeable modifier, you can drag elements that, when released, animate two or more anchor points, usually defined in one direction. A common use is to implement a “slide off” mode. It is important to note that this modifier does not move elements, but only detects gestures. You need to save the state and represent it on the screen, for example by moving elements with the offset modifier. Take a look at the code for Swipeable

@ExperimentalMaterialApi
fun <T> Modifier.swipeable(
    state: SwipeableState<T>,
    anchors: Map<Float, T>,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    interactionSource: MutableInteractionSource? = null,
    thresholds: (from: T.to: T) - >ThresholdConfig = { _, _ -> FixedThreshold(56.dp)}, resistance: ResistanceConfig? = resistanceConfig(anchors.keys), velocityThreshold: Dp = VelocityThreshold ){... }Copy the code
  • Get rememberSwipeableState() for the current offset, current value, animateTo, or snapTo to slide to the specified value, etc.
  • Anchors anchor point.
  • Orientation direction
  • Enabled Whether it is available
  • ReverseDirection Specifies whether the direction is reversed
  • The interactionSource can get the user’s state, such as whether it’s pressed and whether it’s focused. We talked about this before when I introduced Button. [Button’s Explanation]
  • Thresholds Indicates the position of the threshold between states. Let’s say the threshold is 0.3 when we go from 0 to 1. So when we slide less than 0.3 from the start position and let go, it will automatically slide back to the start position. If you release your hand above 0.3, it will automatically slide to the 1 position.
  • Resistance – Controls the amount of resistance applied when brushing across boundaries
  • VelocityThreshold Threshold (in dp per second) at which the end speed must be exceeded in order for the animation to move to the next state, even if the position threshold is not reached.

For example, we used swipeable to experiment with switch controls. The switch can be turned on or off by sliding or clicking. The code is as follows:

@ExperimentalMaterialApi
@Preview
@Composable
fun SwipeableSample(a) {
    val width = 96.dp
    val squareSize = 48.dp

    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
    val scope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .width(width)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { from, to ->
                    if(from==0){
                        FractionalThreshold(0.3 f)}else{
                        FractionalThreshold(0.7 f)}}, orientation = Orientation.Horizontal ) .background(Color.LightGray) ) { Box( Modifier .offset { IntOffset(swipeableState.offset.value.roundToInt(),0) }
                .size(squareSize)
                .background(Color.DarkGray)
                .clickable {
                    scope.launch {
                        if(swipeableState.currentValue==0){
                            swipeableState.animateTo(1)}else{
                            swipeableState.animateTo(0}}})}}Copy the code

The effect is as follows:

Multi-touch: pan, zoom, rotation

To detect multi-touch gestures for panning, scaling, and rotation, you can use the Transformable modifier. This modifier does not transform elements by itself, but only detects gestures. Take a look at the Modifier. Transformable code

fun Modifier.transformable(
    state: TransformableState,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true
){... }Copy the code
  • State TransformableState state access method, obtained through rememberTransformableState rememberTransformableState has three into the ginseng, 1 scale changed much, 2 translation changed much, 3 rotating changed much. The state can be invoked by animatePanBy, animateZoomBy animateRotateBy. PanBy, zoomBy, rotateBy manually panning, zooming, rotating. An animate beginning is an animation.
  • LockRotationOnZoomPan true disables rotation when panning or scaling.
  • Enabled Whether it is available

Example: Double finger zoom, pan, rotate a blue Box. The code is as follows:

@Preview
@Composable
fun transformableTest(a){
    // 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,lockRotationOnZoomPan = false)
            .background(Color.Blue)
            .fillMaxSize()
    )
}
Copy the code

The effect is as follows:

Eight: Modifier. PointerInput gesture detector

PointerInput is a gesture detector, so let’s look at the Modifier

fun Modifier.pointerInput(
    block: suspend PointerInputScope. () - >Unit
){... }Copy the code
  • Block is PointerInputScope. PointerInputScope has the following extension methods and an internal method awaitPointerEventScope:
    • DetectTapGestures listens for long press, click, double click, press
    • DetectDragGestures can listen for drags.
    • DetectHorizontalDragGestures can be monitored when horizontal drag
    • DetectVerticalDragGestures can drag when listening in vertical direction
    • After detectDragGesturesAfterLongPress can listen long according to the drag
    • DetectTransformGestures detects translation, scaling, and rotation
    • ForEachGesture traverses each set of events.
    • awaitPointerEventScope

Let’s do it one by one

8.1 detectTapGestures can listen for long press, click, press, double click

Let’s look at the code for detectTapGestures

suspend fun PointerInputScope.detectTapGestures(
    onDoubleTap: ((Offset) - >Unit)? = null,
    onLongPress: ((Offset) - >Unit)? = null,
    onPress: suspend PressGestureScope. (Offset) - >Unit = NoPressGesture,
    onTap: ((Offset) - >Unit)? = null
){... }Copy the code
  • OnDoubleTap double-click callback
  • OnLongPress long press callback
  • OnPress press
  • OnTap click

For example: the Box turns black when you click, blue when you hold down, red when you double click, and yellow when you press down. The code looks like this:

@Preview
@Composable
fun detectTapGesturesTest(a){
    val color = remember {
        mutableStateOf(Color.Gray)
    }
    Box(modifier = Modifier
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = {
                    Log.e("ccm"."=onDoubleTap==")
                    // Double click to turn red
                    color.value = Color.Red
                },
                onLongPress = {
                    Log.e("ccm"."==onLongPress==")
                    // Long press to turn blue
                    color.value = Color.Blue
                },
                onPress = {
                    Log.e("ccm"."==onPress==")
                    // Press to turn yellow
                    color.value = Color.Yellow
                },
                onTap = {
                    Log.e("ccm"."==onTap==")
                    // Turn black when clicked
                    color.value = Color.Black
                }
            )
        }
        .size(200.dp)
        .background(color.value)){
    }
}
Copy the code

8.2 detectDragGestures detectHorizontalDragGestures detectVerticalDragGestures, detectDragGesturesAfterLongPress. Drag to monitor

Monitoring, detectDragGestures is dragging detectHorizontalDragGestures is the horizontal drag of listening, detectVerticalDragGestures is dragging listening in on the vertical direction, DetectDragGesturesAfterLongPress is according to the drag after listening. Let’s look at their code in detail

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) - >Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange.dragAmount: Offset) - >Unit
){... }suspend fun PointerInputScope.detectVerticalDragGestures(
    onDragStart: (Offset) - >Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onVerticalDrag: (change: PointerInputChange.dragAmount: Float) - >Unit
){... }suspend fun PointerInputScope.detectHorizontalDragGestures(
    onDragStart: (Offset) - >Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onHorizontalDrag: (change: PointerInputChange.dragAmount: Float) - >Unit
){... }suspend fun PointerInputScope.detectDragGesturesAfterLongPress(
    onDragStart: (Offset) - >Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange.dragAmount: Offset) - >Unit
){... }Copy the code
  • OnDragStart Starts dragging
  • OnDragEnd ends the drag
  • OnDragCancel Cancels the drag
  • Drag the onDrag
  • OnVerticalDrag Drag vertically
  • OnHorizontalDrag drag in the horizontal direction

For example, if the Box moves with a finger, the code looks like this:

@Preview
@Composable
fun detectDragGesturesTest(a){
    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(
                        onDragStart = {
                            Log.e("ccm"."Start dragging ===")
                        },
                        onDragEnd = {
                            Log.e("ccm"."The end of the = = =")
                        },
                        onDragCancel = {
                            Log.e("ccm"."Cancel the = = =")
                        },
                        onDrag = { change, dragAmount ->
                            Log.e("ccm"."Dragging ===")
                            change.consumeAllChanges()
                            offsetX += dragAmount.x
                            offsetY += dragAmount.y
                        }
                    )
                }
        )
    }
}
Copy the code

The effect is as follows:

8.3 detectTransformGestures for translation, scaling, and rotation

DetectTransformGestures is designed to detect translation, scaling, rotation. Let’s look at its code in detail:

suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean = false,
    onGesture: (centroid: Offset.pan: Offset.zoom: Float.rotation: Float) - >Unit
){... }Copy the code
  • PanZoomLock true disables rotation when panning or panning.
  • OnGesture is a panning, zooming, and rotating callback listener, and centroid is the coordinate of the center point. Pan is pan, zoom is zoom, rotation is rotation

Examples: support single point movement, multi – point zoom, pan, rotation examples. The code is as follows:

@Preview
@Composable
fun detectTransformGesturesTest(a){
    var scale by remember { mutableStateOf(1f)}var m_rotation by remember { mutableStateOf(0f)}var offset by remember { mutableStateOf(Offset.Zero) }
    Box(
        Modifier
            .graphicsLayer(
                scaleX = scale,
                scaleY = scale,
                rotationZ = m_rotation,
                translationX = offset.x,
                translationY = offset.y
            )
            .pointerInput(Unit) {
                detectTransformGestures(
                    panZoomLock = false. onGesture = { center,pan,zoom,rotation-> scale *= zoom m_rotation += rotation offset += pan } ) } .background(Color.Blue) .fillMaxSize() ) }Copy the code

The renderings are as follows:

8.4 awaitPointerEventScope (listens for each event)

AwaitPointerEventScope is used to listen for an event. Let’s start with the code for awaitPointerEventScope

suspend fun <R> awaitPointerEventScope(
        block: suspend AwaitPointerEventScope. () - >R
    ): R
Copy the code
  • A block is an AwaitPointerEventScope, and AwaitPointerEventScope describes the following methods:
    • Listen for the awaitFirstDown() Down event.
    • AwaitDragOrCancellation () // Drag the cancelled callback
    • AwaitHorizontalDragOrCancellation () / / horizontal drag cancelled the callback
    • AwaitVerticalDragOrCancellation () / / vertical drag cancelled callback
    • Drag () // Drag listener
    • HorizontalDrag () // listen for horizontalDrag
    • VerticalDrag () // verticalDrag listener
    • AwaitTouchSlopOrCancellation () is used to determine whether more than the minimum sliding distance.
    • AwaitVerticalTouchSlopOrCancellation () is used to determine whether more than the minimum on vertical sliding distance.
    • AwaitHorizontalTouchSlopOrCancellation () is used to determine whether more than the minimum on horizontal sliding distance
8.4.1 awaitFirstDown (Listening for Down Events)

AwaitFirstDown is a listener for the Down event.

suspend fun AwaitPointerEventScope.awaitFirstDown(
    requireUnconsumed: Boolean = true
): PointerInputChange {
    var event: PointerEvent
    do {
        event = awaitPointerEvent()
    } while (
        !event.changes.fastAll {
            if (requireUnconsumed) it.changedToDown() else it.changedToDownIgnoreConsumed()
        }
    )
    return event.changes[0]}Copy the code
  • RequireUnconsumed, if requireUnconsumed is true and the first down is used in the pointerEventpass.main procedure, the gesture is ignored.

The return value is a PointerInputChange. Take a look at the code for PointerInputChange:

@Immutable
class PointerInputChange(
    val id: PointerId,
    val uptimeMillis: Long.val position: Offset,
    val pressed: Boolean.val previousUptimeMillis: Long.val previousPosition: Offset,
    val previousPressed: Boolean.val consumed: ConsumedData,
    valtype: PointerType = PointerType.Touch ){... }Copy the code

You can see that the PointerInputChange contains the event ID, position, and other information. Let’s take an example, let’s draw a dot on the screen. Where we press our finger, the dot moves to the point where we press. The code is as follows:

@Preview
@Composable
fun Gesture(a) {
    val offset = remember { Animatable(Offset(0f.0f), Offset.VectorConverter) }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        // Detect a tap event and obtain its position.
                        val position = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            // Animate to the tap position.
                            offset.animateTo(position)
                        }
                    }
                }
            }
    ) {
        Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
    }
}

@Composable
fun Circle(modifier: Modifier){
    Canvas(modifier = modifier) {
        drawCircle(color = Color.Red,radius = 20f,center=Offset(10f.10f))}}private fun Offset.toIntOffset(a) = IntOffset(x.roundToInt(), y.roundToInt())
Copy the code
8.4.2 drag (), horizontalDrag(),verticalDrag(),awaitDragOrCancellation(),awaitHorizontalDragOrCancellation(),awaitVerticalDragOrCancell ation()

A drag listens for a Move event, whereas a horizontalDrag listens for a Move event, and a verticalDrag listens for a vertical Move event. AwaitDragOrCancellation is the listener that the drag cancels. AwaitHorizontalDragOrCancellation is the horizontal drag cancelled listening in. AwaitVerticalDragOrCancellation is vertical drag cancelled listening in. Take a look at their code:

suspend fun AwaitPointerEventScope.drag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) - >Unit
){... }suspend fun AwaitPointerEventScope.horizontalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) - >Unit
){... }suspend fun AwaitPointerEventScope.verticalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) - >Unit
){... }suspend fun AwaitPointerEventScope.awaitDragOrCancellation(
    pointerId: PointerId.): PointerInputChange? {... }suspend fun AwaitPointerEventScope.awaitVerticalDragOrCancellation(
    pointerId: PointerId.): PointerInputChange? {... }suspend fun AwaitPointerEventScope.awaitHorizontalDragOrCancellation(
    pointerId: PointerId.): PointerInputChange? {... }Copy the code
  • PointerId is the ID of the event that is being moved
  • OnDrag is a drag change listener. There is a change parameter that is the change value of pointerput change

When awaitDragOrCancellation awaitVerticalDragOrCancellation, awaitHorizontalDragOrCancellation returns null explain the corresponding tracking id of the event has been raised. Let’s take an example: let’s take a Box and drag it with your finger.

@Preview
@Composable
fun dragTest(a){
    val cacheOffset = remember() {
        mutableStateOf(Offset.Zero)
    }
    val offsetAnimatable = remember { Animatable(Offset(0f.0f), Offset.VectorConverter) }
    Box(
        Modifier
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        / / the down event
                        val downPointerInputChange = awaitPointerEventScope {
                            awaitFirstDown()
                        }
                        offsetAnimatable.stop()
                        // If the position is not where the finger was pressed, animate it to where the finger was pressed
                        if(cacheOffset.value.x ! = downPointerInputChange.position.x && cacheOffset.value.y ! = downPointerInputChange.position.y ) { launch { offsetAnimatable.animateTo(downPointerInputChange.position) cacheOffset.value = downPointerInputChange.position } }// Touch Move event
                        // When you swipe, the box moves with your finger
                        awaitPointerEventScope {
                            drag(downPointerInputChange.id, onDrag = {
                                launch {
                                    offsetAnimatable.snapTo(it.position)
                                }
                                cacheOffset.value = it.position
                            })
                        }

                        // When the finger is bouncing, the animation is used to return to the original position
                        val dragUpOrCancelPointerInputChange = awaitPointerEventScope {
                            awaitDragOrCancellation(downPointerInputChange.id)
                        }
                        // If it is empty, it has been lifted
                        if(dragUpOrCancelPointerInputChange==null){
                            launch {
                                val result = offsetAnimatable.animateTo(Offset.Zero)
                                cacheOffset.value = Offset.Zero
                            }
                        }
                    }
                }
            }
            .fillMaxSize()
    ){
        Box(modifier = Modifier.offset{ IntOffset(offsetAnimatable.value.x.roundToInt(), offsetAnimatable.value.y.roundToInt()) }.size(50.dp).background(Color.Blue))
    }
}
Copy the code

The initial effect is as follows:Here’s another example of a horizontalDrag, a horizontal slide Box. The code is as follows:

@Preview
@Composable
fun swipeToDismissTest(a){
    Column() {
        Box(modifier=Modifier.swipeToDismiss(onDismissed={
            Log.e("ccm"."===onDismissed==")
        }).background(Color.Blue).fillMaxWidth().height(100.dp)){
        }
    }
}

fun Modifier.swipeToDismiss(
    onDismissed: () -> Unit
): Modifier = composed {
    val offsetX = remember { Animatable(0f) }
    pointerInput(Unit) {
        val decay = splineBasedDecay<Float> (this)
        coroutineScope {
            while (true) {
                // Detect a touch down event.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                val velocityTracker = VelocityTracker()
                // Intercept an ongoing animation (if there's one).
                offsetX.stop()
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // Update the animation value with touch events.launch { offsetX.snapTo( offsetX.value + change.positionChange().x ) } velocityTracker.addPosition( change.uptimeMillis,  change.position ) } }val velocity = velocityTracker.calculateVelocity().x
                val targetOffsetX = decay.calculateTargetValue(
                    offsetX.value,
                    velocity
                )
                // The animation stops when it reaches the bounds.
                offsetX.updateBounds(
                    lowerBound = -size.width.toFloat(),
                    upperBound = size.width.toFloat()
                )
                launch {
                    if (targetOffsetX.absoluteValue <= size.width) {
                        // Not enough velocity; Slide back.
                        offsetX.animateTo(
                            targetValue = 0f,
                            initialVelocity = velocity
                        )
                    } else {
                        // The element was swiped away.
                        offsetX.animateDecay(velocity, decay)
                        onDismissed()
                    }
                }
            }
        }
    }.offset { IntOffset(offsetX.value.roundToInt(), 0)}}Copy the code

The initial effect is as follows:

AwaitVerticalTouchSlopOrCancellation 8.4.3 awaitTouchSlopOrCancellation (), (), awaitHorizontalTouchSlopOrCancellation ()

AwaitTouchSlopOrCancellation () is used to determine whether to achieve the minimum sliding distance, awaitVerticalTouchSlopOrCancellation () is used to determine whether reached the minimum distance between sliding on the vertical direction, AwaitHorizontalTouchSlopOrCancellation () is used to determine whether a horizontal reached the minimum sliding distance. Let’s take a look at their specific application scenarios. Remember to speak before a PointerInputScope. DetectDragGestures method. Take a look at the source code for this method.

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) - >Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange.dragAmount: Offset) - >Unit
) {
    forEachGesture {
        awaitPointerEventScope {
            val down = awaitFirstDown(requireUnconsumed = false)
            var drag: PointerInputChange?
            do {
                drag = awaitTouchSlopOrCancellation(down.id, onDrag)
            } while(drag ! =null && !drag.positionChangeConsumed())
            if(drag ! =null) {
                onDragStart.invoke(drag.position)
                if (
                    !drag(drag.id) {
                        onDrag(it, it.positionChange())
                    }
                ) {
                    onDragCancel()
                } else {
                    onDragEnd()
                }
            }
        }
    }
}
Copy the code

Let’s see, forEachGesture is a gesture that is processed repeatedly. If forEachGesture is not added, then the finger detects and walks again. We’ll talk about that later. Val Down = awaitFirstDown(requireUnconsumed = false) Drag = awaitTouchSlopOrCancellation (the id, onDrag) whether this way to get the minimum sliding event, if achieved, then the computer will not is empty, if not reach, drag will be empty. So here while (drag! = null && ! Drag. PositionChangeConsumed ()) will cycle of judgment. Until not empty and the event is not consumed. Ondragstart. invoke(drag.position) to start the slide callback, followed by onDrag, onDragCancel, and onDragEnd

8.5 forEachGesture (Repeatedly manipulating gestures)

ForEachGesture is the repeated processing of gestures. Next, let’s look at forEachGesture’s code

suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope. () - >Unit){... }Copy the code
  • A block is a PointerInputScope. That is, all the methods of the PointerInputScope can be used in forEachGesture.

ForEachGesture means repeatedly processing gestures. What does that mean? Here’s an example:

@Preview
@Composable
fun test(a){
    Column(modifier = Modifier.pointerInput(Unit) {
        
            awaitPointerEventScope {
                val id = awaitFirstDown().id
                Log.e("ccm"."==awaitFirstDown==id===${id}= = =")

                drag(id,onDrag = {
                    Log.e("ccm==onDrag="."====id===${it.id}===position===${it.position}===changedToUp===${it.changedToUp()}==changeToDown==${it.changedToUp()}")
                })
            }
        
    }.fillMaxSize().background(Color.Red))
}
Copy the code

The code above, when we slide over Column, will print out the log of awaitFirstDown and onDrag. But when we lift our finger and slide Column again, we find that log is not hit. That means there’s only one tap. If we want to listen for that gesture over and over again. We can add forEachGesture. The code is modified as follows:

@Preview
@Composable
fun forEachGestureTest(a){
    Column(modifier = Modifier.pointerInput(Unit) {
        forEachGesture {
            awaitPointerEventScope {
                val id = awaitFirstDown().id
                Log.e("ccm"."==awaitFirstDown==id===${id}= = =")

                drag(id,onDrag = {
                    Log.e("ccm==onDrag="."====id===${it.id}===position===${it.position}= = =")
                })
            }
        }
    }.fillMaxSize().background(Color.Red))
}
Copy the code

Now, if WE hit Column again and slide it, we’ll see log. If you lift your finger and click slide again, you still get log. This is forEachGesture at work.