Skip to content

Commit 5f4f93d

Browse files
fix pointerInput and remember vararg keys
1 parent b74ba42 commit 5f4f93d

File tree

7 files changed

+253
-23
lines changed

7 files changed

+253
-23
lines changed

image/src/main/java/com/smarttoolfactory/image/zoom/AnimatedZoomModifier.kt

Lines changed: 231 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,129 @@
11
package com.smarttoolfactory.image.zoom
22

33
import androidx.compose.foundation.gestures.detectTapGestures
4-
import androidx.compose.runtime.rememberCoroutineScope
4+
import androidx.compose.runtime.*
55
import androidx.compose.ui.Modifier
66
import androidx.compose.ui.composed
77
import androidx.compose.ui.draw.clipToBounds
88
import androidx.compose.ui.geometry.Offset
99
import androidx.compose.ui.graphics.graphicsLayer
1010
import androidx.compose.ui.input.pointer.pointerInput
11+
import androidx.compose.ui.platform.debugInspectorInfo
1112
import com.smarttoolfactory.gesture.detectTransformGestures
1213
import com.smarttoolfactory.image.util.getNextZoomLevel
1314
import com.smarttoolfactory.image.util.update
1415
import 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
)

image/src/main/java/com/smarttoolfactory/image/zoom/AnimatedZoomState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ fun rememberAnimatedZoomState(
185185

186186
val density = LocalDensity.current
187187

188-
return remember(keys) {
188+
return remember(*keys) {
189189

190190
val size = if (contentSize == DpSize.Zero) {
191191
IntSize.Zero

image/src/main/java/com/smarttoolfactory/image/zoom/EnhancedZoomModifier.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.smarttoolfactory.image.zoom
22

33
import androidx.compose.foundation.gestures.detectTapGestures
4-
import androidx.compose.runtime.rememberCoroutineScope
4+
import androidx.compose.runtime.*
55
import androidx.compose.ui.Modifier
66
import androidx.compose.ui.composed
77
import androidx.compose.ui.draw.clipToBounds
@@ -55,7 +55,7 @@ fun Modifier.enhancedZoom(
5555
val coroutineScope = rememberCoroutineScope()
5656

5757
// Current Zoom level
58-
var zoomLevel = ZoomLevel.Min
58+
var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }
5959

6060
// Whether panning should be limited to bounds of gesture area or not
6161
val boundPan = enhancedZoomState.limitPan && !enhancedZoomState.rotatable
@@ -115,8 +115,8 @@ fun Modifier.enhancedZoom(
115115
detectTapGestures(
116116
onDoubleTap = {
117117
coroutineScope.launch {
118-
val newZoom = zoomOnDoubleTap(zoomLevel)
119118
zoomLevel = getNextZoomLevel(zoomLevel)
119+
val newZoom = zoomOnDoubleTap(zoomLevel)
120120
enhancedZoomState.onDoubleTap(zoom = newZoom) {
121121
onGestureEnd?.invoke(enhancedZoomState.enhancedZoomData)
122122
}
@@ -187,7 +187,7 @@ fun Modifier.enhancedZoom(
187187
factory = {
188188
val coroutineScope = rememberCoroutineScope()
189189
// Current Zoom level
190-
var zoomLevel = ZoomLevel.Min
190+
var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }
191191

192192
// Whether panning should be limited to bounds of gesture area or not
193193
val boundPan = enhancedZoomState.limitPan && !enhancedZoomState.rotatable
@@ -246,8 +246,8 @@ fun Modifier.enhancedZoom(
246246
detectTapGestures(
247247
onDoubleTap = {
248248
coroutineScope.launch {
249-
val newZoom = zoomOnDoubleTap(zoomLevel)
250249
zoomLevel = getNextZoomLevel(zoomLevel)
250+
val newZoom = zoomOnDoubleTap(zoomLevel)
251251
enhancedZoomState.onDoubleTap(zoom = newZoom) {
252252
onGestureEnd?.invoke(enhancedZoomState.enhancedZoomData)
253253
}
@@ -281,7 +281,6 @@ fun Modifier.enhancedZoom(
281281
}
282282
)
283283

284-
285284
/**
286285
* Modifier that zooms in or out of Composable set to. This zoom modifier has option
287286
* to move back to bounds with an animation or option to have fling gesture when user removes
@@ -322,15 +321,15 @@ fun Modifier.enhancedZoom(
322321
val coroutineScope = rememberCoroutineScope()
323322

324323
// Current Zoom level
325-
var zoomLevel = ZoomLevel.Min
324+
var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }
326325

327326
// Whether panning should be limited to bounds of gesture area or not
328327
val boundPan = enhancedZoomState.limitPan && !enhancedZoomState.rotatable
329328

330329
// If we bound to touch area or clip is true Modifier.clipToBounds is used
331330
val clipToBounds = (clip || boundPan)
332331

333-
val transformModifier = Modifier.pointerInput(keys) {
332+
val transformModifier = Modifier.pointerInput(*keys) {
334333
// Pass size of this Composable this Modifier is attached for constraining operations
335334
// inside this bounds
336335
enhancedZoomState.size = this.size
@@ -374,15 +373,15 @@ fun Modifier.enhancedZoom(
374373
)
375374
}
376375

377-
val tapModifier = Modifier.pointerInput(keys) {
376+
val tapModifier = Modifier.pointerInput(*keys) {
378377
// Pass size of this Composable this Modifier is attached for constraining operations
379378
// inside this bounds
380379
enhancedZoomState.size = this.size
381380
detectTapGestures(
382381
onDoubleTap = {
383382
coroutineScope.launch {
384-
val newZoom = zoomOnDoubleTap(zoomLevel)
385383
zoomLevel = getNextZoomLevel(zoomLevel)
384+
val newZoom = zoomOnDoubleTap(zoomLevel)
386385
enhancedZoomState.onDoubleTap(zoom = newZoom) {
387386
onGestureEnd?.invoke(enhancedZoomState.enhancedZoomData)
388387
}

image/src/main/java/com/smarttoolfactory/image/zoom/EnhancedZoomState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ fun rememberEnhancedZoomState(
167167
limitPan: Boolean = false,
168168
vararg keys: Any?
169169
): EnhancedZoomState {
170-
return remember(keys) {
170+
return remember(*keys) {
171171
EnhancedZoomState(
172172
imageSize = imageSize,
173173
initialZoom = initialZoom,

image/src/main/java/com/smarttoolfactory/image/zoom/EnhancedZoomableImage.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ fun EnhancedZoomableImage(
7777

7878
val zoomModifier = Modifier
7979
.enhancedZoom(
80+
key1= imageBitmap,
81+
key2 = contentScale,
8082
enhancedZoomState = rememberEnhancedZoomState(
83+
key1 = imageBitmap,
84+
key2 = contentScale,
8185
imageSize = IntSize(imageBitmap.width, imageBitmap.height),
8286
initialZoom = initialZoom,
8387
minZoom = minZoom,

0 commit comments

Comments
 (0)