11package com.smarttoolfactory.image.zoom
22
3- import androidx.compose.animation.core.Animatable
4- import androidx.compose.animation.core.VectorConverter
5- import androidx.compose.animation.core.spring
63import androidx.compose.foundation.Image
7- import androidx.compose.foundation.gestures.detectTapGestures
8- import androidx.compose.runtime.*
4+ import androidx.compose.runtime.Composable
95import androidx.compose.ui.Alignment
106import androidx.compose.ui.Modifier
11- import androidx.compose.ui.draw.clipToBounds
12- import androidx.compose.ui.geometry.Offset
13- import androidx.compose.ui.geometry.Size
14- import androidx.compose.ui.graphics.*
7+ import androidx.compose.ui.graphics.ColorFilter
8+ import androidx.compose.ui.graphics.DefaultAlpha
9+ import androidx.compose.ui.graphics.FilterQuality
10+ import androidx.compose.ui.graphics.ImageBitmap
1511import androidx.compose.ui.graphics.drawscope.DrawScope
16- import androidx.compose.ui.input.pointer.pointerInput
1712import androidx.compose.ui.layout.ContentScale
18- import androidx.compose.ui.platform.LocalDensity
19- import com.smarttoolfactory.gesture.detectTransformGestures
2013import com.smarttoolfactory.image.ImageWithConstraints
21- import kotlinx.coroutines.launch
2214
2315/* *
2416 * Zoomable image that zooms in and out in [ [minZoom], [maxZoom] ] interval and translates
@@ -30,6 +22,19 @@ import kotlinx.coroutines.launch
3022 * @param maxZoom maximum zoom value this Composable can possess
3123 * @param clipTransformToContentScale when set true zoomable image takes borders of image drawn
3224 * while zooming in. [contentScale] determines whether will be empty spaces on edges of Composable
25+ * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
26+ * empty space on sides or edges of parent.
27+ * @param consume flag to prevent other gestures such as scroll, drag or transform to get
28+ * event propagations
29+ * @param zoomEnabled when set to true zoom is enabled
30+ * @param panEnabled when set to true pan is enabled
31+ * @param rotationEnabled when set to true rotation is enabled
32+ * @param onGestureStart callback to to notify gesture has started and return current ZoomData
33+ * of this modifier
34+ * @param onGesture callback to notify about ongoing gesture and return current ZoomData
35+ * of this modifier
36+ * @param onGestureEnd callback to notify that gesture finished and return current ZoomData
37+ * of this modifier
3338 */
3439@Composable
3540fun ZoomableImage (
@@ -42,79 +47,36 @@ fun ZoomableImage(
4247 initialZoom : Float = 1f,
4348 minZoom : Float = 1f,
4449 maxZoom : Float = 5f,
50+ limitPan : Boolean = true,
51+ zoomEnabled : Boolean = true,
52+ panEnabled : Boolean = true,
53+ rotationEnabled : Boolean = false,
54+ consume : Boolean = true,
55+ onGestureStart : (ZoomData ) -> Unit = {},
56+ onGesture : (ZoomData ) -> Unit = {},
57+ onGestureEnd : (ZoomData ) -> Unit = {},
4558 clipTransformToContentScale : Boolean = false,
4659 colorFilter : ColorFilter ? = null,
4760 filterQuality : FilterQuality = DrawScope .DefaultFilterQuality ,
4861) {
4962
50- val coroutineScope = rememberCoroutineScope()
51- val zoomMin = minZoom.coerceAtLeast(.5f )
52- val zoomMax = maxZoom.coerceAtLeast(1f )
53- val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)
54-
55- require(zoomMax >= zoomMin)
56-
57- var size by remember { mutableStateOf(Size .Zero ) }
58-
59- val animatableOffset = remember(imageBitmap, contentScale) {
60- Animatable (Offset .Zero , Offset .VectorConverter )
61- }
62- val animatableZoom = remember(imageBitmap, contentScale) { Animatable (zoomInitial) }
63-
6463 val zoomModifier = Modifier
65- .clipToBounds()
66- .graphicsLayer {
67- val zoom = animatableZoom.value
68- translationX = animatableOffset.value.x
69- translationY = animatableOffset.value.y
70- scaleX = zoom
71- scaleY = zoom
72- }
73- .pointerInput(imageBitmap, contentScale) {
74-
75- detectTransformGestures(
76- onGesture = { _,
77- gesturePan: Offset ,
78- gestureZoom: Float ,
79- _,
80- _,
81- _ ->
82-
83- var zoom = animatableZoom.value
84- val offset = animatableOffset.value
85-
86- zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
87- val newOffset = offset + gesturePan.times(zoom)
88-
89- val maxX = (size.width * (zoom - 1 ) / 2f ).coerceAtLeast(0f )
90- val maxY = (size.height * (zoom - 1 ) / 2f ).coerceAtLeast(0f )
91-
92- coroutineScope.launch {
93- animatableZoom.snapTo(zoom)
94- }
95- coroutineScope.launch {
96- animatableOffset.snapTo(
97- Offset (
98- newOffset.x.coerceIn(- maxX, maxX),
99- newOffset.y.coerceIn(- maxY, maxY)
100- )
101- )
102- }
103- }
104- )
105- }
106- .pointerInput(imageBitmap, contentScale) {
107- detectTapGestures(
108- onDoubleTap = {
109- coroutineScope.launch {
110- animatableOffset.animateTo(Offset .Zero , spring())
111- }
112- coroutineScope.launch {
113- animatableZoom.animateTo(zoomInitial, spring())
114- }
115- }
116- )
117- }
64+ .zoom(
65+ Unit ,
66+ limitPan = limitPan,
67+ zoomEnabled = zoomEnabled,
68+ panEnabled = panEnabled,
69+ rotationEnabled = rotationEnabled,
70+ zoomState = rememberZoomState(
71+ initialZoom = initialZoom,
72+ minZoom = minZoom,
73+ maxZoom = maxZoom
74+ ),
75+ consume = consume,
76+ onGestureStart = onGestureStart,
77+ onGesture = onGesture,
78+ onGestureEnd = onGestureEnd
79+ )
11880
11981 ImageWithConstraints (
12082 modifier = if (clipTransformToContentScale) modifier else modifier.then(zoomModifier),
@@ -128,12 +90,6 @@ fun ZoomableImage(
12890 drawImage = ! clipTransformToContentScale
12991 ) {
13092
131- size = with (LocalDensity .current) {
132- Size (
133- width = imageWidth.toPx(),
134- height = imageHeight.toPx()
135- )
136- }
13793
13894 if (clipTransformToContentScale) {
13995 Image (
0 commit comments