Skip to content

Commit fe675c8

Browse files
update ZoomModifier with rotation
1 parent 60442bb commit fe675c8

File tree

1 file changed

+164
-79
lines changed

1 file changed

+164
-79
lines changed

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

Lines changed: 164 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import androidx.compose.animation.core.Animatable
44
import androidx.compose.animation.core.VectorConverter
55
import androidx.compose.animation.core.spring
66
import androidx.compose.foundation.gestures.detectTapGestures
7-
import androidx.compose.runtime.remember
8-
import androidx.compose.runtime.rememberCoroutineScope
7+
import androidx.compose.runtime.*
98
import androidx.compose.ui.Modifier
109
import androidx.compose.ui.composed
1110
import androidx.compose.ui.draw.clipToBounds
@@ -19,12 +18,24 @@ import kotlinx.coroutines.launch
1918
* Modifier that zooms in or out of Composable set to.
2019
* @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
2120
* change
21+
* @param initialZoom initial value of zoom
22+
* @param minZoom minimum zoom value
23+
* @param maxZoom maximum zoom value
2224
* @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
2325
* drawn
2426
* @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
2527
* empty space on sides or edges of parent.
26-
* @param minZoom minimum zoom value
27-
* @param maxZoom maximum zoom value
28+
* @param consume flag to prevent other gestures such as scroll, drag or transform to get
29+
* event propagations
30+
* @param zoomEnabled when set to true zoom is enabled
31+
* @param panEnabled when set to true pan is enabled
32+
* @param rotationEnabled when set to true rotation is enabled
33+
* @param onGestureStart callback to to notify gesture has started and return current ZoomData
34+
* of this modifier
35+
* @param onGesture callback to notify about ongoing gesture and return current ZoomData
36+
* of this modifier
37+
* @param onGestureEnd callback to notify that gesture finished and return current ZoomData
38+
* of this modifier
2839
*/
2940
fun Modifier.zoom(
3041
vararg keys: Any?,
@@ -34,9 +45,12 @@ fun Modifier.zoom(
3445
clip: Boolean = true,
3546
limitPan: Boolean = true,
3647
consume: Boolean = true,
37-
onGestureStart: () -> Unit = {},
38-
onChange: (Zoom) -> Unit = {},
39-
onGestureEnd: () -> Unit = {},
48+
zoomEnabled: Boolean = true,
49+
panEnabled: Boolean = true,
50+
rotationEnabled: Boolean = false,
51+
onGestureStart: (ZoomData) -> Unit = {},
52+
onGesture: (ZoomData) -> Unit = {},
53+
onGestureEnd: (ZoomData) -> Unit = {},
4054
) = composed(
4155
factory = {
4256

@@ -45,90 +59,159 @@ fun Modifier.zoom(
4559
val zoomMax = maxZoom.coerceAtLeast(1f)
4660
val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)
4761

48-
4962
require(zoomMax >= zoomMin)
5063

51-
val animatableOffset = remember {
64+
val animatablePan = remember {
5265
Animatable(Offset.Zero, Offset.VectorConverter)
5366
}
5467
val animatableZoom = remember { Animatable(zoomInitial) }
68+
val animatableRotation = remember { Animatable(0f) }
5569

56-
this.then(
57-
(if (clip) Modifier.clipToBounds() else Modifier)
58-
.graphicsLayer {
59-
val zoom = animatableZoom.value
60-
val translationX = animatableOffset.value.x
61-
val translationY = animatableOffset.value.y
62-
this.translationX = translationX
63-
this.translationY = translationY
64-
scaleX = zoom
65-
scaleY = zoom
66-
67-
onChange(
68-
Zoom(
69-
zoom = zoom,
70-
translationX = translationX,
71-
translationY
70+
var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }
71+
72+
val boundPan = limitPan && !rotationEnabled
73+
val clipToBounds = (clip || boundPan)
74+
75+
76+
val transformModifier = Modifier.pointerInput(keys) {
77+
detectTransformGestures(
78+
consume = consume,
79+
onGestureStart = {
80+
onGestureStart(
81+
ZoomData(
82+
zoom = animatableZoom.value,
83+
pan = animatablePan.value,
84+
rotation = animatableRotation.value
7285
)
7386
)
74-
}
75-
.pointerInput(keys) {
76-
detectTransformGestures(
77-
consume = consume,
78-
onGestureStart = {
79-
onGestureStart()
80-
},
81-
onGestureEnd = {
82-
onGestureEnd()
83-
},
84-
onGesture = { _,
85-
gesturePan: Offset,
86-
gestureZoom: Float,
87-
_,
88-
_,
89-
_ ->
90-
91-
println("🔥 PointerInput size: $size")
92-
93-
var zoom = animatableZoom.value
94-
val offset = animatableOffset.value
95-
96-
zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
97-
val newOffset = offset + gesturePan.times(zoom)
98-
99-
val maxX = (size.width * (zoom - 1) / 2f).coerceAtLeast(0f)
100-
val maxY = (size.height * (zoom - 1) / 2f).coerceAtLeast(0f)
101-
102-
coroutineScope.launch {
103-
animatableZoom.snapTo(zoom)
104-
}
105-
coroutineScope.launch {
106-
animatableOffset.snapTo(
107-
if (limitPan) {
108-
Offset(
109-
newOffset.x.coerceIn(-maxX, maxX),
110-
newOffset.y.coerceIn(-maxY, maxY)
111-
)
112-
} else {
113-
newOffset
114-
}
115-
)
116-
}
87+
},
88+
onGestureEnd = {
89+
onGestureEnd(
90+
ZoomData(
91+
zoom = animatableZoom.value,
92+
pan = animatablePan.value,
93+
rotation = animatableRotation.value
94+
)
95+
)
96+
},
97+
onGesture = { centroid, gesturePan, gestureZoom, gestureRotate,
98+
_,
99+
_ ->
100+
101+
var zoom = animatableZoom.value
102+
val offset = animatablePan.value
103+
val rotation = if (rotationEnabled) {
104+
animatableRotation.value + gestureRotate
105+
} else {
106+
0f
107+
}
108+
109+
if (zoomEnabled) {
110+
zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
111+
}
112+
113+
val newOffset = offset + gesturePan.times(zoom)
114+
115+
val maxX = (size.width * (zoom - 1) / 2f)
116+
.coerceAtLeast(0f)
117+
val maxY = (size.height * (zoom - 1) / 2f)
118+
.coerceAtLeast(0f)
119+
120+
121+
if (zoomEnabled) {
122+
coroutineScope.launch {
123+
animatableZoom.snapTo(zoom)
124+
}
125+
}
126+
127+
if (panEnabled) {
128+
coroutineScope.launch {
129+
animatablePan.snapTo(
130+
if (boundPan) {
131+
Offset(
132+
newOffset.x.coerceIn(-maxX, maxX),
133+
newOffset.y.coerceIn(-maxY, maxY)
134+
)
135+
} else {
136+
newOffset
137+
}
138+
)
139+
}
140+
}
141+
142+
if (rotationEnabled) {
143+
coroutineScope.launch {
144+
animatableRotation.snapTo(rotation)
117145
}
146+
}
147+
148+
onGesture(
149+
ZoomData(
150+
zoom = animatableZoom.value,
151+
pan = animatablePan.value,
152+
rotation = animatableRotation.value
153+
)
118154
)
119155
}
120-
.pointerInput(keys) {
121-
detectTapGestures(
122-
onDoubleTap = {
123-
coroutineScope.launch {
124-
animatableOffset.animateTo(Offset.Zero, spring())
125-
}
126-
coroutineScope.launch {
127-
animatableZoom.animateTo(zoomInitial, spring())
128-
}
129-
}
156+
)
157+
}
158+
159+
val tapModifier = Modifier.pointerInput(keys) {
160+
detectTapGestures(
161+
onDoubleTap = {
162+
163+
val (newZoomLevel, newZoom) = calculateZoom(
164+
zoomLevel = zoomLevel,
165+
initial = zoomInitial,
166+
min = minZoom,
167+
max = maxZoom
130168
)
169+
170+
zoomLevel = newZoomLevel
171+
172+
coroutineScope.launch {
173+
animatablePan.animateTo(Offset.Zero, spring())
174+
}
175+
coroutineScope.launch {
176+
animatableZoom.animateTo(newZoom, spring())
177+
}
178+
coroutineScope.launch {
179+
animatableRotation.animateTo(0f, spring())
180+
}
131181
}
182+
)
183+
}
184+
185+
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+
)
207+
}
208+
209+
this.then(
210+
(if (clipToBounds) Modifier.clipToBounds() else Modifier)
211+
.then(transformModifier)
212+
.then(tapModifier)
213+
.then(graphicsModifier)
214+
132215
)
133216

134217
},
@@ -153,6 +236,7 @@ fun Modifier.zoom(
153236
initialZoom: Float = 1f,
154237
minZoom: Float = 1f,
155238
maxZoom: Float = 5f,
239+
rotate: Boolean = false,
156240
clip: Boolean = true,
157241
limitPan: Boolean = true,
158242

@@ -163,8 +247,9 @@ fun Modifier.zoom(
163247
maxZoom = maxZoom,
164248
clip = clip,
165249
limitPan = limitPan,
250+
rotationEnabled = rotate,
166251
consume = true,
167252
onGestureStart = {},
168253
onGestureEnd = {},
169-
onChange = {}
254+
onGesture = {}
170255
)

0 commit comments

Comments
 (0)