2

I am trying to implement a composable which allows the user to drag and zoom an image. It seems that the most natural way to implement this is by using the pointerInput or transformable modifiers. However, both do not seem to support fling. If I try it with scrollable instead, then fling works but one can only drag in one orientation at the same time (even if I add scrollable with both orientations).

I think my use case it not completely uncommon (see e.g. MapView, which needs something similar), so I am a bit confused why Jetpack Compose does not support it properly. Am I missing something?

Lucas Mann
  • 377
  • 1
  • 3
  • 7
  • Fling? Do you mean pan? – Richard Onslow Roper Aug 15 '21 at 12:00
  • By fling I mean that if you pan with force and then lift the finger, the panning should continue, gradually slowing down. This is the standard behavior of scrollable views like lists. – Lucas Mann Aug 15 '21 at 12:13
  • 1
    Oh, I don't think it's called fling exactly, but yes, this can be achieved by `decay` animation. – Richard Onslow Roper Aug 15 '21 at 12:27
  • Ahh thanks, I wasn't aware of `animateDecay`. It even says in the docs that this can be used to implement fling behavior! (Still, I wasn't able to find this by searching "fling animation"...) I'll figure out how to implement this in my use case later and post an answer here. – Lucas Mann Aug 15 '21 at 12:32
  • By searching for `animateDecay` I found that my question is actually answered in the docs (not in the gestures tab, but in the animation tab). Now I feel a bit stupid... Thanks anyways! – Lucas Mann Aug 15 '21 at 12:40

1 Answers1

0

Sure! Here's the translation of the code and explanation for your Stack Overflow answer:

To track the velocity of your finger and initiate an animation when the drag is released, you can use a VelocityTracker. Here is an example code:

val tracker = remember { VelocityTracker() }

pointerInput(enabled, horizontalReverseScrolling, verticalReverseScrolling) {
    do {
        val event = awaitPointerEvent()
        val canceled = event.changes.fastAny { it.isConsumed }

        if (!pastTouchSlop) {
                zoom *= zoomChange
                rotation += rotationChange
                pan += panChange

                val centroidSize = event.calculateCentroidSize(useCurrent = false)
                val zoomMotion = abs(1 - zoom) * centroidSize
                val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
                val panMotion = pan.getDistance()

                if (zoomMotion > touchSlop ||
                    rotationMotion > touchSlop ||
                    panMotion > touchSlop
                ) {
                    pastTouchSlop = true
                    lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
                }
            }

            if (pastTouchSlop) {
                val centroid = event.calculateCentroid(useCurrent = false)
                val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
                if (effectiveRotation != 0f ||
                    zoomChange != 1f ||
                    panChange != Offset.Zero
                ) {
                    onGesture(centroid, panChange, zoomChange, effectiveRotation, dragEvent)
                    
                    
                     tracker.addPointerInputChange(change)
                }
                event.changes.fastForEach {
                    if (it.positionChanged()) {
                        it.consume()
                    }
                }
            }
        }
    } while (!canceled && event.changes.fastAny { it.pressed })

    // Call the callback function and pass the velocity when the drag ends
    val velocity = tracker.calculateVelocity()
    tracker.resetTracking()
    onEnd(Offset(tracker.x, tracker.y))
}

Finally, in the callback function, start the animation using the velocity information. Here is an example of how to use rememberSplineBasedDecay and AnimationState to create the animation:

val flingSpec = rememberSplineBasedDecay<Offset>()
launch {
    AnimationState(
        typeConverter = Offset.VectorConverter,
        initialValue = Offset.Zero,
        initialVelocity = velocityOffset,
    ).animateDecay(flingSpec) { animationValue ->
        val delta = animationValue - lastValue
        // Handle the offset value in the animation callback
        onGesture(offset = delta)
        lastValue = animationValue
    }
}
midFang
  • 96
  • 3