Transformables with fling?

953 Views Asked by At

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?

1

There are 1 best solutions below

0
On

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
    }
}