Skip to content

Commit 49aa9d3

Browse files
update ZoomModifier
1 parent d2daf9c commit 49aa9d3

File tree

1 file changed

+120
-67
lines changed

1 file changed

+120
-67
lines changed

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

Lines changed: 120 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@ 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.*
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.rememberCoroutineScope
89
import androidx.compose.ui.Modifier
910
import androidx.compose.ui.composed
11+
import androidx.compose.ui.draw.clipToBounds
1012
import androidx.compose.ui.geometry.Offset
11-
import androidx.compose.ui.geometry.Size
1213
import androidx.compose.ui.graphics.graphicsLayer
1314
import androidx.compose.ui.input.pointer.pointerInput
14-
import androidx.compose.ui.layout.onSizeChanged
15-
import androidx.compose.ui.unit.toSize
1615
import com.smarttoolfactory.gesture.detectTransformGestures
17-
import com.smarttoolfactory.image.transform.Transform
1816
import kotlinx.coroutines.launch
1917

2018
/**
2119
* Modifier that zooms in or out of Composable set to.
2220
* @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
2321
* change
24-
* @param initialZoom zoom set initially
22+
* @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
23+
* drawn
24+
* @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
25+
* empty space on sides or edges of parent.
2526
* @param minZoom minimum zoom value
2627
* @param maxZoom maximum zoom value
2728
*/
@@ -31,7 +32,11 @@ fun Modifier.zoom(
3132
minZoom: Float = 1f,
3233
maxZoom: Float = 5f,
3334
clip: Boolean = true,
34-
onChange: (Transform) -> Unit = {}
35+
limitPan: Boolean = true,
36+
consume: Boolean = true,
37+
onGestureStart: () -> Unit = {},
38+
onChange: (Zoom) -> Unit = {},
39+
onGestureEnd: () -> Unit = {},
3540
) = composed(
3641
factory = {
3742

@@ -40,78 +45,126 @@ fun Modifier.zoom(
4045
val zoomMax = maxZoom.coerceAtLeast(1f)
4146
val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)
4247

43-
require(zoomMax >= zoomMin)
44-
45-
var size by remember { mutableStateOf(Size.Zero) }
4648

49+
require(zoomMax >= zoomMin)
4750

4851
val animatableOffset = remember {
4952
Animatable(Offset.Zero, Offset.VectorConverter)
5053
}
5154
val animatableZoom = remember { Animatable(zoomInitial) }
5255

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)
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
72+
)
73+
)
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+
}
92115
)
93-
)
94-
}
95-
}
96-
)
97-
}
98-
.pointerInput(keys) {
99-
detectTapGestures(
100-
onDoubleTap = {
101-
coroutineScope.launch {
102-
animatableOffset.animateTo(Offset.Zero, spring())
116+
}
103117
}
104-
coroutineScope.launch {
105-
animatableZoom.animateTo(zoomInitial, spring())
118+
)
119+
}
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+
}
106129
}
107-
}
108-
)
109-
}
110-
.onSizeChanged {
111-
size = it.toSize()
112-
}
130+
)
131+
}
132+
)
133+
113134
},
114135
inspectorInfo = {
115136

116137
}
138+
)
139+
140+
/**
141+
* Modifier that zooms in or out of Composable set to.
142+
* @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
143+
* change
144+
* @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
145+
* drawn
146+
* @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
147+
* empty space on sides or edges of parent.
148+
* @param minZoom minimum zoom value
149+
* @param maxZoom maximum zoom value
150+
*/
151+
fun Modifier.zoom(
152+
vararg keys: Any?,
153+
initialZoom: Float = 1f,
154+
minZoom: Float = 1f,
155+
maxZoom: Float = 5f,
156+
clip: Boolean = true,
157+
limitPan: Boolean = true,
158+
159+
) = zoom(
160+
keys = keys,
161+
initialZoom = initialZoom,
162+
minZoom = minZoom,
163+
maxZoom = maxZoom,
164+
clip = clip,
165+
limitPan = limitPan,
166+
consume = true,
167+
onGestureStart = {},
168+
onGestureEnd = {},
169+
onChange = {}
117170
)

0 commit comments

Comments
 (0)