From b018b1c37890f13a20714f90bf177f9f598cc2b8 Mon Sep 17 00:00:00 2001 From: Arne Jans Date: Wed, 6 Apr 2022 11:11:31 +0200 Subject: [PATCH 1/2] optionally disable drag-gestures in not-zoomed state This improves page-swiping in HorizontalPager or horizontal lists. --- .../kotlin/de/mr_pine/zoomables/Zoomables.kt | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt index be6acb2..9615fb4 100644 --- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt +++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt @@ -34,6 +34,7 @@ import kotlin.math.* * * @param coroutineScope used for smooth asynchronous zoom/pan/rotation animations * @param zoomableState Contains the current transform states - obtained via [rememberZoomableState] + * @param swipeEnabled Enable user swipes in the unzoomed state - enabled by default * @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default * @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default * @param minimumSwipeDistance Minimum distance the user has to travel on the screen for it to count as swiping @@ -44,6 +45,7 @@ import kotlin.math.* public fun Zoomable( coroutineScope: CoroutineScope, zoomableState: ZoomableState, + swipeEnabled: Boolean = true, onSwipeLeft: () -> Unit = {}, onSwipeRight: () -> Unit = {}, minimumSwipeDistance: Int = 0, @@ -198,40 +200,42 @@ public fun Zoomable( transformEventCounter++ } while (!canceled && event.changes.fastAny { it.pressed } && relevant) - do { - awaitPointerEvent() - drag = awaitTouchSlopOrCancellation(down.id) { change, over -> - change.consumePositionChange() - overSlop = over - } - } while (drag != null && !drag.positionChangeConsumed()) - if (drag != null) { - dragOffset = Offset.Zero - if (zoomableState.scale.value !in 0.92f..1.08f) { - coroutineScope.launch { - zoomableState.transform { - transformBy(1f, overSlop, 0f) - } + if (swipeEnabled || zoomableState.scale.value !in 0.92f..1.08f) { + do { + awaitPointerEvent() + drag = awaitTouchSlopOrCancellation(down.id) { change, over -> + change.consumePositionChange() + overSlop = over } - } else { - dragOffset += overSlop - } - if (drag(drag.id) { - if (zoomableState.scale.value !in 0.92f..1.08f) { - zoomableState.offset.value += it.positionChange() - } else { - dragOffset += it.positionChange() + } while (drag != null && !drag.positionChangeConsumed()) + if (drag != null) { + dragOffset = Offset.Zero + if (zoomableState.scale.value !in 0.92f..1.08f) { + coroutineScope.launch { + zoomableState.transform { + transformBy(1f, overSlop, 0f) + } } - it.consumePositionChange() + } else { + dragOffset += overSlop } - ) { - if (zoomableState.scale.value in 0.92f..1.08f) { - val offsetX = dragOffset.x - if (offsetX > minimumSwipeDistance) { - onSwipeRight() + if (drag(drag.id) { + if (zoomableState.scale.value !in 0.92f..1.08f) { + zoomableState.offset.value += it.positionChange() + } else { + dragOffset += it.positionChange() + } + it.consumePositionChange() + } + ) { + if (zoomableState.scale.value in 0.92f..1.08f) { + val offsetX = dragOffset.x + if (offsetX > minimumSwipeDistance) { + onSwipeRight() - } else if (offsetX < -minimumSwipeDistance) { - onSwipeLeft() + } else if (offsetX < -minimumSwipeDistance) { + onSwipeLeft() + } } } } From d5cd4ba5a22852255c8d19c41e1b3533aeed3bb2 Mon Sep 17 00:00:00 2001 From: "Mr. Pine" <50425705+Mr-Pine@users.noreply.github.com> Date: Thu, 7 Apr 2022 19:04:04 +0200 Subject: [PATCH 2/2] Bump Version to 1.1.2, Fix some documentation errors, swipeEnabled is now dragGesturesEnabled and is of type () -> Boolean --- zoomables/build.gradle | 2 +- .../de/mr_pine/zoomables/ZoomableImage.kt | 9 +++++++++ .../de/mr_pine/zoomables/ZoomableState.kt | 19 +++++++++++++++---- .../kotlin/de/mr_pine/zoomables/Zoomables.kt | 8 ++++---- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/zoomables/build.gradle b/zoomables/build.gradle index 75aecb4..09148a3 100644 --- a/zoomables/build.gradle +++ b/zoomables/build.gradle @@ -57,7 +57,7 @@ dependencies { ext{ PUBLISH_GROUP_ID = 'de.mr-pine.utils' - PUBLISH_VERSION = '1.1.1' + PUBLISH_VERSION = '1.1.2' PUBLISH_ARTIFACT_ID = 'zoomables' } diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt index 4a453be..ef31f84 100644 --- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt +++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.CoroutineScope * @param contentDescription text for accessibility see [Image] for further info * @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default * @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default + * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case * @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default) */ @Composable @@ -34,6 +35,7 @@ public fun ZoomableImage( contentDescription: String? = null, onSwipeLeft: () -> Unit = {}, onSwipeRight: () -> Unit = {}, + dragGesturesEnabled: ZoomableState.() -> Boolean = { true }, onDoubleTap: ((Offset) -> Unit)? = null ) { Zoomable( @@ -41,6 +43,7 @@ public fun ZoomableImage( zoomableState = zoomableState, onSwipeLeft = onSwipeLeft, onSwipeRight = onSwipeRight, + dragGesturesEnabled = dragGesturesEnabled, onDoubleTap = onDoubleTap ) { Image(bitmap = bitmap, contentDescription = contentDescription, modifier = modifier) @@ -57,6 +60,7 @@ public fun ZoomableImage( * @param contentDescription text for accessibility see [Image] for further info * @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default * @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default + * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case * @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default) */ @Composable @@ -68,6 +72,7 @@ public fun ZoomableImage( contentDescription: String? = null, onSwipeLeft: () -> Unit = {}, onSwipeRight: () -> Unit = {}, + dragGesturesEnabled: ZoomableState.() -> Boolean = { true }, onDoubleTap: ((Offset) -> Unit)? = null ) { Zoomable( @@ -75,6 +80,7 @@ public fun ZoomableImage( zoomableState = zoomableState, onSwipeLeft = onSwipeLeft, onSwipeRight = onSwipeRight, + dragGesturesEnabled = dragGesturesEnabled, onDoubleTap = onDoubleTap ) { Image( @@ -95,6 +101,7 @@ public fun ZoomableImage( * @param contentDescription text for accessibility see [Image] for further info * @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default * @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default + * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case * @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default) */ @Composable @@ -106,6 +113,7 @@ public fun ZoomableImage( contentDescription: String? = null, onSwipeLeft: () -> Unit = {}, onSwipeRight: () -> Unit = {}, + dragGesturesEnabled: ZoomableState.() -> Boolean = { true }, onDoubleTap: ((Offset) -> Unit)? = null ) { Zoomable( @@ -113,6 +121,7 @@ public fun ZoomableImage( zoomableState = zoomableState, onSwipeLeft = onSwipeLeft, onSwipeRight = onSwipeRight, + dragGesturesEnabled = dragGesturesEnabled, onDoubleTap = onDoubleTap ) { Image(painter = painter, contentDescription = contentDescription, modifier = modifier) diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt index edd9298..54a49d5 100644 --- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt +++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt @@ -28,12 +28,12 @@ import kotlin.math.sqrt * * @param onTransformation callback invoked when transformation occurs. The callback receives the * change from the previous event. It's relative scale multiplier for zoom, [Offset] in pixels - * for pan and degrees for rotation. If this parameter is null the default behaviour is - * zooming, panning and rotating by the supplied changes. Rotation is kept between positive and negative 180 + * for pan and degrees for rotation. * * @property scale The current scale as [MutableState]<[Float]> * @property offset The current offset as [MutableState]<[Offset]> * @property rotation The current rotation in degrees as [MutableState]<[Float]> + * @property notTransformed `true` if [scale] is `1`, [offset] is [Offset.Zero] and [rotation] is `0` */ public class ZoomableState( public var scale: MutableState, @@ -42,6 +42,12 @@ public class ZoomableState( public val rotationBehavior: Rotation, onTransformation: ZoomableState.(zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit ) : TransformableState { + + public val notTransformed: Boolean + get() { + return scale.value in (1 - 1.0E-3f)..(1 + 1.0E-3f) && offset.value.getDistanceSquared() in -1.0E-6f..1.0E-6f && rotation.value in -1.0E-3f..1.0E-3f + } + private val transformScope: TransformScope = object : TransformScope { override fun transformBy(zoomChange: Float, panChange: Offset, rotationChange: Float) = onTransformation(zoomChange, panChange, rotationChange) @@ -51,6 +57,7 @@ public class ZoomableState( private val isTransformingState = mutableStateOf(false) + override suspend fun transform( transformPriority: MutatePriority, block: suspend TransformScope.() -> Unit @@ -87,7 +94,11 @@ public class ZoomableState( } } - public suspend fun animateZoomToPosition(zoomChange: Float, position: Offset, currentComposableCenter: Offset = Offset.Zero) { + public suspend fun animateZoomToPosition( + zoomChange: Float, + position: Offset, + currentComposableCenter: Offset = Offset.Zero + ) { val offsetBuffer = offset.value val x0 = position.x - currentComposableCenter.x @@ -143,7 +154,7 @@ public class ZoomableState( * * @param onTransformation callback invoked when transformation occurs. The callback receives the * change from the previous event. It's relative scale multiplier for zoom, [Offset] in pixels - * for pan and degrees for rotation. If this parameter is null the default behaviour is + * for pan and degrees for rotation. If not provided the default behaviour is * zooming, panning and rotating by the supplied changes. Rotation is kept between positive and negative 180 * * @return A [ZoomableState] initialized with the given [initialZoom], [initialOffset] and [initialRotation] diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt index 9615fb4..e9edc98 100644 --- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt +++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt @@ -34,18 +34,18 @@ import kotlin.math.* * * @param coroutineScope used for smooth asynchronous zoom/pan/rotation animations * @param zoomableState Contains the current transform states - obtained via [rememberZoomableState] - * @param swipeEnabled Enable user swipes in the unzoomed state - enabled by default + * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case * @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default * @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default * @param minimumSwipeDistance Minimum distance the user has to travel on the screen for it to count as swiping - * @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x to the touch point when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default) + * @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x to the touch point when scale is currently 1 and zooms out to scale = 1 when zoomed in when `null` (default) */ @Composable public fun Zoomable( coroutineScope: CoroutineScope, zoomableState: ZoomableState, - swipeEnabled: Boolean = true, + dragGesturesEnabled: ZoomableState.() -> Boolean = { true }, onSwipeLeft: () -> Unit = {}, onSwipeRight: () -> Unit = {}, minimumSwipeDistance: Int = 0, @@ -200,7 +200,7 @@ public fun Zoomable( transformEventCounter++ } while (!canceled && event.changes.fastAny { it.pressed } && relevant) - if (swipeEnabled || zoomableState.scale.value !in 0.92f..1.08f) { + if (zoomableState.dragGesturesEnabled()) { do { awaitPointerEvent() drag = awaitTouchSlopOrCancellation(down.id) { change, over ->