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.gestures.detectTapGestures
7- import androidx.compose.runtime.*
4+ import androidx.compose.runtime.rememberCoroutineScope
85import androidx.compose.ui.Modifier
96import androidx.compose.ui.composed
107import androidx.compose.ui.draw.clipToBounds
118import androidx.compose.ui.geometry.Offset
9+ import androidx.compose.ui.graphics.GraphicsLayerScope
1210import androidx.compose.ui.graphics.graphicsLayer
1311import androidx.compose.ui.input.pointer.pointerInput
1412import com.smarttoolfactory.gesture.detectTransformGestures
@@ -18,9 +16,6 @@ import kotlinx.coroutines.launch
1816 * Modifier that zooms in or out of Composable set to.
1917 * @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
2018 * change
21- * @param initialZoom initial value of zoom
22- * @param minZoom minimum zoom value
23- * @param maxZoom maximum zoom value
2419 * @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
2520 * drawn
2621 * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
@@ -39,75 +34,44 @@ import kotlinx.coroutines.launch
3934 */
4035fun Modifier.zoom (
4136 vararg keys : Any? ,
42- initialZoom : Float = 1f,
43- minZoom : Float = 1f,
44- maxZoom : Float = 5f,
4537 clip : Boolean = true,
4638 limitPan : Boolean = true,
4739 consume : Boolean = true,
4840 zoomEnabled : Boolean = true,
4941 panEnabled : Boolean = true,
5042 rotationEnabled : Boolean = false,
43+ zoomState : ZoomState ,
5144 onGestureStart : (ZoomData ) -> Unit = {},
5245 onGesture : (ZoomData ) -> Unit = {},
5346 onGestureEnd : (ZoomData ) -> Unit = {},
5447) = composed(
5548 factory = {
56-
5749 val coroutineScope = rememberCoroutineScope()
58- val zoomMin = minZoom.coerceAtLeast(.5f )
59- val zoomMax = maxZoom.coerceAtLeast(1f )
60- val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)
61-
62- require(zoomMax >= zoomMin)
63-
64- val animatablePan = remember {
65- Animatable (Offset .Zero , Offset .VectorConverter )
66- }
67- val animatableZoom = remember { Animatable (zoomInitial) }
68- val animatableRotation = remember { Animatable (0f ) }
69-
70- var zoomLevel by remember { mutableStateOf(ZoomLevel .Min ) }
71-
7250 val boundPan = limitPan && ! rotationEnabled
7351 val clipToBounds = (clip || boundPan)
7452
75-
7653 val transformModifier = Modifier .pointerInput(keys) {
7754 detectTransformGestures(
7855 consume = consume,
7956 onGestureStart = {
80- onGestureStart(
81- ZoomData (
82- zoom = animatableZoom.value,
83- pan = animatablePan.value,
84- rotation = animatableRotation.value
85- )
86- )
57+ onGestureStart(zoomState.zoomData)
8758 },
8859 onGestureEnd = {
89- onGestureEnd(
90- ZoomData (
91- zoom = animatableZoom.value,
92- pan = animatablePan.value,
93- rotation = animatableRotation.value
94- )
95- )
60+ onGestureEnd(zoomState.zoomData)
9661 },
97- onGesture = { centroid, gesturePan, gestureZoom, gestureRotate,
98- _,
99- _ ->
62+ onGesture = { _, gesturePan, gestureZoom, gestureRotate, _, _ ->
63+
64+ var zoom = zoomState.zoom
65+ val offset = zoomState.pan
10066
101- var zoom = animatableZoom.value
102- val offset = animatablePan.value
10367 val rotation = if (rotationEnabled) {
104- animatableRotation.value + gestureRotate
68+ zoomState.rotation + gestureRotate
10569 } else {
10670 0f
10771 }
10872
10973 if (zoomEnabled) {
110- zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
74+ zoom = (zoom * gestureZoom).coerceIn(zoomState. zoomMin, zoomState. zoomMax)
11175 }
11276
11377 val newOffset = offset + gesturePan.times(zoom)
@@ -117,16 +81,15 @@ fun Modifier.zoom(
11781 val maxY = (size.height * (zoom - 1 ) / 2f )
11882 .coerceAtLeast(0f )
11983
120-
12184 if (zoomEnabled) {
12285 coroutineScope.launch {
123- animatableZoom.snapTo (zoom)
86+ zoomState.snapZoomTo (zoom)
12487 }
12588 }
12689
12790 if (panEnabled) {
12891 coroutineScope.launch {
129- animatablePan.snapTo (
92+ zoomState.snapPanTo (
13093 if (boundPan) {
13194 Offset (
13295 newOffset.x.coerceIn(- maxX, maxX),
@@ -141,17 +104,11 @@ fun Modifier.zoom(
141104
142105 if (rotationEnabled) {
143106 coroutineScope.launch {
144- animatableRotation.snapTo (rotation)
107+ zoomState.snapRotationTo (rotation)
145108 }
146109 }
147110
148- onGesture(
149- ZoomData (
150- zoom = animatableZoom.value,
151- pan = animatablePan.value,
152- rotation = animatableRotation.value
153- )
154- )
111+ onGesture(zoomState.zoomData)
155112 }
156113 )
157114 }
@@ -161,49 +118,31 @@ fun Modifier.zoom(
161118 onDoubleTap = {
162119
163120 val (newZoomLevel, newZoom) = calculateZoom(
164- zoomLevel = zoomLevel,
165- initial = zoomInitial,
166- min = minZoom ,
167- max = maxZoom
121+ zoomLevel = zoomState. zoomLevel,
122+ initial = zoomState. zoomInitial,
123+ min = zoomState.zoomMin ,
124+ max = zoomState.zoomMax
168125 )
169126
170- zoomLevel = newZoomLevel
127+ zoomState. zoomLevel = newZoomLevel
171128
172129 coroutineScope.launch {
173- animatablePan.animateTo (Offset .Zero , spring() )
130+ zoomState.animatePanTo (Offset .Zero )
174131 }
132+
175133 coroutineScope.launch {
176- animatableZoom.animateTo (newZoom, spring() )
134+ zoomState.animateZoomTo (newZoom)
177135 }
136+
178137 coroutineScope.launch {
179- animatableRotation.animateTo( 0f , spring() )
138+ zoomState.animateRotationTo(zoomState.rotationInitial )
180139 }
181140 }
182141 )
183142 }
184143
185144 val graphicsModifier = Modifier .graphicsLayer {
186- val zoom = animatableZoom.value
187-
188- // Set zoom
189- scaleX = zoom
190- scaleY = zoom
191-
192- // Set pan
193- val translationX = animatablePan.value.x
194- val translationY = animatablePan.value.y
195- this .translationX = translationX
196- this .translationY = translationY
197-
198- // Set rotation
199- rotationZ = animatableRotation.value
200- // TransformOrigin(0f, 0f).also { transformOrigin = it }
201- onGesture(
202- ZoomData (
203- zoom = animatableZoom.value,
204- pan = animatablePan.value,
205- )
206- )
145+ this .update(zoomState)
207146 }
208147
209148 this .then(
@@ -213,10 +152,19 @@ fun Modifier.zoom(
213152 .then(graphicsModifier)
214153
215154 )
216-
217155 },
218156 inspectorInfo = {
219-
157+ name = " zoom"
158+ properties[" keys" ] = keys
159+ properties[" clip" ] = clip
160+ properties[" limitPan" ] = limitPan
161+ properties[" consume" ] = consume
162+ properties[" zoomEnabled" ] = zoomEnabled
163+ properties[" rotationEnabled" ] = rotationEnabled
164+ properties[" zoomState" ] = zoomState
165+ properties[" onGestureStart" ] = onGestureStart
166+ properties[" onGesture" ] = onGesture
167+ properties[" onGestureEnd" ] = onGestureEnd
220168 }
221169)
222170
@@ -228,28 +176,40 @@ fun Modifier.zoom(
228176 * drawn
229177 * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
230178 * empty space on sides or edges of parent.
231- * @param minZoom minimum zoom value
232- * @param maxZoom maximum zoom value
233179 */
234180fun Modifier.zoom (
235181 vararg keys : Any? ,
236- initialZoom : Float = 1f,
237- minZoom : Float = 1f,
238- maxZoom : Float = 5f,
239- rotate : Boolean = false,
182+ zoomState : ZoomState ,
183+ rotationEnabled : Boolean = false,
240184 clip : Boolean = true,
241- limitPan : Boolean = true,
242-
243- ) = zoom(
185+ limitPan : Boolean = true
186+ ) = zoom(
244187 keys = keys,
245- initialZoom = initialZoom,
246- minZoom = minZoom,
247- maxZoom = maxZoom,
248188 clip = clip,
249189 limitPan = limitPan,
250- rotationEnabled = rotate ,
190+ rotationEnabled = rotationEnabled ,
251191 consume = true ,
192+ zoomState = zoomState,
252193 onGestureStart = {},
253194 onGestureEnd = {},
254195 onGesture = {}
255- )
196+ )
197+
198+ private fun GraphicsLayerScope.update (zoomState : ZoomState ) {
199+
200+ // Set zoom
201+ val zoom = zoomState.zoom
202+ this .scaleX = zoom
203+ this .scaleY = zoom
204+
205+ // Set pan
206+ val pan = zoomState.pan
207+ val translationX = pan.x
208+ val translationY = pan.y
209+ this .translationX = translationX
210+ this .translationY = translationY
211+
212+ // Set rotation
213+ this .rotationZ = zoomState.rotation
214+ }
215+
0 commit comments