11package com.smarttoolfactory.image.zoom
22
33import androidx.compose.foundation.gestures.detectTapGestures
4- import androidx.compose.runtime.rememberCoroutineScope
4+ import androidx.compose.runtime.*
55import androidx.compose.ui.Modifier
66import androidx.compose.ui.composed
77import androidx.compose.ui.draw.clipToBounds
88import androidx.compose.ui.geometry.Offset
99import androidx.compose.ui.graphics.graphicsLayer
1010import androidx.compose.ui.input.pointer.pointerInput
11+ import androidx.compose.ui.platform.debugInspectorInfo
1112import com.smarttoolfactory.gesture.detectTransformGestures
1213import com.smarttoolfactory.image.util.getNextZoomLevel
1314import com.smarttoolfactory.image.util.update
1415import kotlinx.coroutines.launch
1516
17+ /* *
18+ * Modifier that zooms in or out of Composable set to. This zoom modifier has option
19+ * to move back to bounds with an animation or option to have fling gesture when user removes
20+ * from screen while velocity is higher than threshold to have smooth touch effect.
21+ *
22+ * @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
23+ * change
24+ * @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
25+ * drawn
26+ * @param animatedZoomState State of the zoom that contains option to set initial, min, max zoom,
27+ * enabling rotation, pan or zoom
28+ * @param zoomOnDoubleTap lambda that returns current [ZoomLevel] and based on current level
29+ * enables developer to define zoom on double tap gesture
30+ * @param enabled lambda can be used selectively enable or disable pan and intercepting with
31+ * scroll, drag or lists or pagers using current zoom, pan or rotation values
32+ */
33+ fun Modifier.animatedZoom (
34+ vararg keys : Any? ,
35+ clip : Boolean = true,
36+ animatedZoomState : AnimatedZoomState ,
37+ enabled : (Float , Offset , Float ) -> Boolean = DefaultEnabled ,
38+ zoomOnDoubleTap : (ZoomLevel ) -> Float = animatedZoomState.DefaultOnDoubleTap ,
39+ ) = composed(
40+
41+ factory = {
42+
43+ val coroutineScope = rememberCoroutineScope()
44+
45+ // Current Zoom level
46+ var zoomLevel by remember { mutableStateOf(ZoomLevel .Min ) }
47+
48+ // Whether panning should be limited to bounds of gesture area or not
49+ val boundPan = animatedZoomState.limitPan && ! animatedZoomState.rotatable
50+
51+ // If we bound to touch area or clip is true Modifier.clipToBounds is used
52+ val clipToBounds = (clip || boundPan)
53+
54+ val transformModifier = Modifier .pointerInput(* keys) {
55+ // Pass size of this Composable this Modifier is attached for constraining operations
56+ // inside this bounds
57+ animatedZoomState.size = this .size
58+ detectTransformGestures(
59+ consume = false ,
60+ onGestureEnd = {
61+ coroutineScope.launch {
62+ animatedZoomState.onGestureEnd {
63+ }
64+ }
65+ },
66+ onGesture = { centroid, pan, zoom, rotate, mainPointer, pointerList ->
67+
68+ val currentZoom = animatedZoomState.zoom
69+ val currentPan = animatedZoomState.pan
70+ val currentRotation = animatedZoomState.rotation
71+ val gestureEnabled = enabled(currentZoom, currentPan, currentRotation)
72+
73+ coroutineScope.launch {
74+ animatedZoomState.onGesture(
75+ centroid = centroid,
76+ pan = if (gestureEnabled) pan else Offset .Zero ,
77+ zoom = zoom,
78+ rotation = rotate,
79+ mainPointer = mainPointer,
80+ changes = pointerList
81+ )
82+
83+ if (gestureEnabled) {
84+ mainPointer.consume()
85+ }
86+ }
87+ }
88+ )
89+ }
90+
91+ val tapModifier = Modifier .pointerInput(* keys) {
92+ // Pass size of this Composable this Modifier is attached for constraining operations
93+ // inside this bounds
94+ animatedZoomState.size = this .size
95+ detectTapGestures(
96+ onDoubleTap = {
97+ coroutineScope.launch {
98+ zoomLevel = getNextZoomLevel(zoomLevel)
99+ val newZoom = zoomOnDoubleTap(zoomLevel)
100+ animatedZoomState.onDoubleTap(zoom = newZoom) {}
101+ }
102+ }
103+ )
104+ }
105+
106+ val graphicsModifier = Modifier .graphicsLayer {
107+ this .update(animatedZoomState)
108+ }
109+
110+ this .then(
111+ (if (clipToBounds) Modifier .clipToBounds() else Modifier )
112+ .then(tapModifier)
113+ .then(transformModifier)
114+ .then(graphicsModifier)
115+ )
116+ },
117+ inspectorInfo = debugInspectorInfo {
118+ name = " animatedZoomState"
119+ properties[" keys" ] = keys
120+ properties[" clip" ] = clip
121+ properties[" animatedZoomState" ] = animatedZoomState
122+ properties[" enabled" ] = enabled
123+ properties[" zoomOnDoubleTap" ] = zoomOnDoubleTap
124+ }
125+ )
126+
16127/* *
17128 * Modifier that zooms in or out of Composable set to. This zoom modifier has option
18129 * to move back to bounds with an animation or option to have fling gesture when user removes
@@ -42,7 +153,7 @@ fun Modifier.animatedZoom(
42153 val coroutineScope = rememberCoroutineScope()
43154
44155 // Current Zoom level
45- var zoomLevel = ZoomLevel .Min
156+ var zoomLevel by remember { mutableStateOf( ZoomLevel .Min ) }
46157
47158 // Whether panning should be limited to bounds of gesture area or not
48159 val boundPan = animatedZoomState.limitPan && ! animatedZoomState.rotatable
@@ -94,8 +205,8 @@ fun Modifier.animatedZoom(
94205 detectTapGestures(
95206 onDoubleTap = {
96207 coroutineScope.launch {
97- val newZoom = zoomOnDoubleTap(zoomLevel)
98208 zoomLevel = getNextZoomLevel(zoomLevel)
209+ val newZoom = zoomOnDoubleTap(zoomLevel)
99210 animatedZoomState.onDoubleTap(zoom = newZoom) {}
100211 }
101212 }
@@ -113,8 +224,124 @@ fun Modifier.animatedZoom(
113224 .then(graphicsModifier)
114225 )
115226 },
116- inspectorInfo = {
227+ inspectorInfo = debugInspectorInfo {
117228 name = " animatedZoomState"
229+ properties[" key" ] = key
230+ properties[" clip" ] = clip
231+ properties[" animatedZoomState" ] = animatedZoomState
232+ properties[" enabled" ] = enabled
233+ properties[" zoomOnDoubleTap" ] = zoomOnDoubleTap
234+ }
235+ )
236+
237+ /* *
238+ * Modifier that zooms in or out of Composable set to. This zoom modifier has option
239+ * to move back to bounds with an animation or option to have fling gesture when user removes
240+ * from screen while velocity is higher than threshold to have smooth touch effect.
241+ *
242+ * [key1], [key2] are used for [Modifier.pointerInput] to restart closure when any keys assigned
243+ * change
244+ * @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
245+ * drawn
246+ * @param animatedZoomState State of the zoom that contains option to set initial, min, max zoom,
247+ * enabling rotation, pan or zoom
248+ * @param zoomOnDoubleTap lambda that returns current [ZoomLevel] and based on current level
249+ * enables developer to define zoom on double tap gesture
250+ * @param enabled lambda can be used selectively enable or disable pan and intercepting with
251+ * scroll, drag or lists or pagers using current zoom, pan or rotation values
252+ */
253+ fun Modifier.animatedZoom (
254+ key1 : Any? = Unit ,
255+ key2 : Any? = Unit ,
256+ clip : Boolean = true,
257+ animatedZoomState : AnimatedZoomState ,
258+ enabled : (Float , Offset , Float ) -> Boolean = DefaultEnabled ,
259+ zoomOnDoubleTap : (ZoomLevel ) -> Float = animatedZoomState.DefaultOnDoubleTap ,
260+ ) = composed(
261+
262+ factory = {
263+
264+ val coroutineScope = rememberCoroutineScope()
265+
266+ // Current Zoom level
267+ var zoomLevel by remember { mutableStateOf(ZoomLevel .Min ) }
268+
269+ // Whether panning should be limited to bounds of gesture area or not
270+ val boundPan = animatedZoomState.limitPan && ! animatedZoomState.rotatable
271+
272+ // If we bound to touch area or clip is true Modifier.clipToBounds is used
273+ val clipToBounds = (clip || boundPan)
274+
275+ val transformModifier = Modifier .pointerInput(key1, key2) {
276+ // Pass size of this Composable this Modifier is attached for constraining operations
277+ // inside this bounds
278+ animatedZoomState.size = this .size
279+ detectTransformGestures(
280+ consume = false ,
281+ onGestureEnd = {
282+ coroutineScope.launch {
283+ animatedZoomState.onGestureEnd {
284+ }
285+ }
286+ },
287+ onGesture = { centroid, pan, zoom, rotate, mainPointer, pointerList ->
118288
289+ val currentZoom = animatedZoomState.zoom
290+ val currentPan = animatedZoomState.pan
291+ val currentRotation = animatedZoomState.rotation
292+ val gestureEnabled = enabled(currentZoom, currentPan, currentRotation)
293+
294+ coroutineScope.launch {
295+ animatedZoomState.onGesture(
296+ centroid = centroid,
297+ pan = if (gestureEnabled) pan else Offset .Zero ,
298+ zoom = zoom,
299+ rotation = rotate,
300+ mainPointer = mainPointer,
301+ changes = pointerList
302+ )
303+
304+ if (gestureEnabled) {
305+ mainPointer.consume()
306+ }
307+ }
308+ }
309+ )
310+ }
311+
312+ val tapModifier = Modifier .pointerInput(key1, key2) {
313+ // Pass size of this Composable this Modifier is attached for constraining operations
314+ // inside this bounds
315+ animatedZoomState.size = this .size
316+ detectTapGestures(
317+ onDoubleTap = {
318+ coroutineScope.launch {
319+ zoomLevel = getNextZoomLevel(zoomLevel)
320+ val newZoom = zoomOnDoubleTap(zoomLevel)
321+ animatedZoomState.onDoubleTap(zoom = newZoom) {}
322+ }
323+ }
324+ )
325+ }
326+
327+ val graphicsModifier = Modifier .graphicsLayer {
328+ this .update(animatedZoomState)
329+ }
330+
331+ this .then(
332+ (if (clipToBounds) Modifier .clipToBounds() else Modifier )
333+ .then(tapModifier)
334+ .then(transformModifier)
335+ .then(graphicsModifier)
336+ )
337+ },
338+ inspectorInfo = debugInspectorInfo {
339+ name = " animatedZoomState"
340+ properties[" key1" ] = key1
341+ properties[" key2" ] = key2
342+ properties[" clip" ] = clip
343+ properties[" animatedZoomState" ] = animatedZoomState
344+ properties[" enabled" ] = enabled
345+ properties[" zoomOnDoubleTap" ] = zoomOnDoubleTap
119346 }
120347)
0 commit comments