Home > Mobile >  Multiple different pointerInput
Multiple different pointerInput

Time:06-28

I'm currently trying to implement the option of switching between a composable being either zoomable, pannable (dragging the surface) or neither of those. What works so far is toggling the respective buttons, with the expected result. What does not work is toggling one button from the other - this gives the unexpected result of keeping the functionality of the first button. For example, let's say zoom is active. When I then press the pan button, the background highlighting changes accordingly, all test-logs show the expected state - but the surface is still zoomable, NOT draggable. I first have to manually disable zoom. Any ideas as to why this might happen?

  Modifier.run {
            if (zoomEnabled) {
                this.pointerInput(Unit) {
                    detectTransformGestures { _, _, zoom, _ ->
                        passScale(zoom)
                    }
                }
            } else if (panEnabled) {
                this.pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        passOffsetX(dragAmount.x / 3)    
                        passOffsetY(dragAmount.y / 3)    
                    }   
                }
            } else
                this
        }

Buttons:

@Composable
fun TopBarAction(
    zoomEnabled: Boolean,
    passZoomEnabled: (Boolean) -> Unit,
    panEnabled: Boolean,
    passPanEnabled: (Boolean) -> Unit
) {
    IconToggleButton(
        checked = zoomEnabled,
        onCheckedChange = {
            passPanEnabled(false)
            passZoomEnabled(it)
        },
        modifier = Modifier
            .background(
                if (zoomEnabled) Color.LightGray else Color.Transparent,
                shape = CircleShape
            ),
        enabled = true
    ) {
        Icon(...)
    }
    IconToggleButton(
        checked = panEnabled,
        onCheckedChange = {
            passZoomEnabled(false)
            passPanEnabled(it)
        },
        modifier = Modifier
            .background(
                if (panEnabled) Color.LightGray else Color.Transparent,
                shape = CircleShape
            ),
        enabled = true
    ) {
        Icon(...)
    }
}

Using the normal .pointerInput modifier with the condition inside instead of run does not recognize any input at all and detectTransformGesture's pan did not behave the way I need it to (though this is probably what I will use if all else fails)

CodePudding user response:

First issue is PointerInput creates a closure with key or keys and uses old values unless the keys you set change.

You need to set keys accordingly.

Second issue is even if you set keys, detectDragGestures or detectTransformGestures will consume events so PointerInputChange above won't get it if first one has already consumed event.

What consume() or consumeAllChanges() does is it prevents pointerInput above it or on parent to receive events by returning PointeInputChange.positionChange() Offset.Zero, PointerInputChange.isConsumed true. Since drag, scroll or transform gestures check if PointeInputChange.isConsumed is true they will never get any event if you consume them in previous pointerInput.

Drag source code for instance

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?
            var overSlop = Offset.Zero
            do {
                drag = awaitPointerSlopOrCancellation(
                    down.id,
                    down.type
                ) { change, over ->
                    change.consume()
                    overSlop = over
                }
            // ! EVERY Default GESTURE HAS THIS CHECK
            } while (drag != null && !drag.isConsumed)
            if (drag != null) {
                onDragStart.invoke(drag.position)
                onDrag(drag, overSlop)
                if (
                    !drag(drag.id) {
                        onDrag(it, it.positionChange())
                        it.consume()
                    }
                ) {
                    onDragCancel()
                } else {
                    onDragEnd()
                }
            }
        }
    }
}

Instead of using Modifier.run you can chain Modifier.pointerInput()

Modifier
  .pointerInput(keys){
    // Gesture scope1
    if(zoomEnabled){...}
  } 
  .pointerInput(keys){
    // Gesture scope2
      if(panEnabled){
        ....
    }

  } 

Events first go to gesture scope 2 then gesture scope 1

Created a small sample that you can observer how gestures change and propagate and how they reset with keys

@Composable
private fun MyComposable() {

    var zoomEnabled by remember { mutableStateOf(false) }
    var dragEnabled by remember { mutableStateOf(false) }
    var text by remember { mutableStateOf("") }

    Column() {
        val modifier = Modifier
            .size(400.dp)
            .background(Color.Red)
            .pointerInput(zoomEnabled) {
              if (zoomEnabled) {
                  detectTransformGestures { centroid, pan, zoom, rotation ->
                      println("ZOOOMING")
                      text = "ZOOMING centroid: $centroid"
                  }
              }
            }
            .pointerInput(key1 = dragEnabled, key2= zoomEnabled) {
                if (dragEnabled && !zoomEnabled) {
                    detectDragGestures { change, dragAmount ->
                        println("DRAGGING")
                        text = "DRAGGING $dragAmount"
                    }
                }
            }
        Box(modifier = modifier)

        Text(text = text)

        OutlinedButton(onClick = { zoomEnabled = !zoomEnabled }) {
            Text("zoomEnabled: $zoomEnabled")
        }

        OutlinedButton(onClick = { dragEnabled = !dragEnabled }) {
            Text("dragEnabled: $dragEnabled")
        }
    }
}

You can create your own behavior using the answer and snippet above.

  • Related