Skip to content

Commit 1ff2541

Browse files
add Modifier.zoom
1 parent 84f5275 commit 1ff2541

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.smarttoolfactory.image.zoom
2+
3+
import androidx.compose.animation.core.Animatable
4+
import androidx.compose.animation.core.VectorConverter
5+
import androidx.compose.animation.core.spring
6+
import androidx.compose.foundation.gestures.detectTapGestures
7+
import androidx.compose.runtime.*
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.composed
10+
import androidx.compose.ui.geometry.Offset
11+
import androidx.compose.ui.geometry.Size
12+
import androidx.compose.ui.graphics.graphicsLayer
13+
import androidx.compose.ui.input.pointer.pointerInput
14+
import androidx.compose.ui.layout.onSizeChanged
15+
import androidx.compose.ui.unit.toSize
16+
import com.smarttoolfactory.gesture.detectTransformGestures
17+
import com.smarttoolfactory.image.transform.Transform
18+
import kotlinx.coroutines.launch
19+
20+
/**
21+
* Modifier that zooms in or out of Composable set to.
22+
* @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
23+
* change
24+
* @param initialZoom zoom set initially
25+
* @param minZoom minimum zoom value
26+
* @param maxZoom maximum zoom value
27+
*/
28+
fun Modifier.zoom(
29+
vararg keys: Any?,
30+
initialZoom: Float = 1f,
31+
minZoom: Float = 1f,
32+
maxZoom: Float = 5f,
33+
clip: Boolean = true,
34+
onChange: (Transform) -> Unit = {}
35+
) = composed(
36+
factory = {
37+
38+
val coroutineScope = rememberCoroutineScope()
39+
val zoomMin = minZoom.coerceAtLeast(.5f)
40+
val zoomMax = maxZoom.coerceAtLeast(1f)
41+
val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)
42+
43+
require(zoomMax >= zoomMin)
44+
45+
var size by remember { mutableStateOf(Size.Zero) }
46+
47+
48+
val animatableOffset = remember {
49+
Animatable(Offset.Zero, Offset.VectorConverter)
50+
}
51+
val animatableZoom = remember { Animatable(zoomInitial) }
52+
53+
Modifier
54+
// .then(if (clip) Modifier.clipToBounds() else Modifier)
55+
.graphicsLayer {
56+
val zoom = animatableZoom.value
57+
translationX = animatableOffset.value.x
58+
translationY = animatableOffset.value.y
59+
scaleX = zoom
60+
scaleY = zoom
61+
this.clip = clip
62+
63+
onChange(Transform(translationX, translationY, scaleX, scaleY))
64+
}
65+
.pointerInput(keys) {
66+
67+
detectTransformGestures(
68+
onGesture = { _,
69+
gesturePan: Offset,
70+
gestureZoom: Float,
71+
_,
72+
_,
73+
_ ->
74+
75+
var zoom = animatableZoom.value
76+
val offset = animatableOffset.value
77+
78+
zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
79+
val newOffset = offset + gesturePan.times(zoom)
80+
81+
val maxX = (size.width * (zoom - 1) / 2f).coerceAtLeast(0f)
82+
val maxY = (size.height * (zoom - 1) / 2f).coerceAtLeast(0f)
83+
84+
coroutineScope.launch {
85+
animatableZoom.snapTo(zoom)
86+
}
87+
coroutineScope.launch {
88+
animatableOffset.snapTo(
89+
Offset(
90+
newOffset.x.coerceIn(-maxX, maxX),
91+
newOffset.y.coerceIn(-maxY, maxY)
92+
)
93+
)
94+
}
95+
}
96+
)
97+
}
98+
.pointerInput(keys) {
99+
detectTapGestures(
100+
onDoubleTap = {
101+
coroutineScope.launch {
102+
animatableOffset.animateTo(Offset.Zero, spring())
103+
}
104+
coroutineScope.launch {
105+
animatableZoom.animateTo(zoomInitial, spring())
106+
}
107+
}
108+
)
109+
}
110+
.onSizeChanged {
111+
size = it.toSize()
112+
}
113+
},
114+
inspectorInfo = {
115+
116+
}
117+
)

0 commit comments

Comments
 (0)