@@ -4,24 +4,25 @@ import androidx.compose.animation.core.Animatable
44import androidx.compose.animation.core.VectorConverter
55import androidx.compose.animation.core.spring
66import androidx.compose.foundation.gestures.detectTapGestures
7- import androidx.compose.runtime.*
7+ import androidx.compose.runtime.remember
8+ import androidx.compose.runtime.rememberCoroutineScope
89import androidx.compose.ui.Modifier
910import androidx.compose.ui.composed
11+ import androidx.compose.ui.draw.clipToBounds
1012import androidx.compose.ui.geometry.Offset
11- import androidx.compose.ui.geometry.Size
1213import androidx.compose.ui.graphics.graphicsLayer
1314import androidx.compose.ui.input.pointer.pointerInput
14- import androidx.compose.ui.layout.onSizeChanged
15- import androidx.compose.ui.unit.toSize
1615import com.smarttoolfactory.gesture.detectTransformGestures
17- import com.smarttoolfactory.image.transform.Transform
1816import 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