@@ -7,26 +7,139 @@ import androidx.compose.runtime.Immutable
77import androidx.compose.runtime.remember
88import androidx.compose.ui.Modifier
99import androidx.compose.ui.geometry.Offset
10+ import androidx.compose.ui.unit.IntSize
1011import kotlinx.coroutines.coroutineScope
1112
13+
14+
15+ /* *
16+ * * Create and [remember] the [ZoomState] based on the currently appropriate transform
17+ * configuration to allow changing pan, zoom, and rotation.
18+ *
19+ * [key1] is used to reset remember block to initial calculations. This can be used
20+ * when image, contentScale or any property changes which requires values to be reset to initial
21+ * values
22+ *
23+ * @param initialZoom zoom set initially
24+ * @param initialRotation rotation set initially
25+ * @param minZoom minimum zoom value this Composable can possess
26+ * @param maxZoom maximum zoom value this Composable can possess
27+ * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
28+ * empty space on sides or edges of parent
29+ * @param zoomEnabled when set to true zoom is enabled
30+ * @param panEnabled when set to true pan is enabled
31+ * @param rotationEnabled when set to true rotation is enabled
32+ */
33+ @Composable
34+ fun rememberZoomState (
35+ initialZoom : Float = 1f,
36+ initialRotation : Float = 0f,
37+ minZoom : Float = 1f,
38+ maxZoom : Float = 5f,
39+ zoomEnabled : Boolean = true,
40+ panEnabled : Boolean = true,
41+ rotationEnabled : Boolean = false,
42+ limitPan : Boolean = false,
43+ key1 : Any? = Unit
44+ ): ZoomState {
45+ return remember(key1) {
46+ ZoomState (
47+ initialZoom = initialZoom,
48+ initialRotation = initialRotation,
49+ minZoom = minZoom,
50+ maxZoom = maxZoom,
51+ zoomEnabled = zoomEnabled,
52+ panEnabled = panEnabled,
53+ rotationEnabled = rotationEnabled,
54+ limitPan = limitPan
55+ )
56+ }
57+ }
58+
1259/* *
1360 * * Create and [remember] the [ZoomState] based on the currently appropriate transform
1461 * configuration to allow changing pan, zoom, and rotation.
1562 *
63+ * [key1] or [key2] are used to reset remember block to initial calculations. This can be used
64+ * when image, contentScale or any property changes which requires values to be reset to initial
65+ * values
66+ *
67+ * @param initialZoom zoom set initially
68+ * @param initialRotation rotation set initially
69+ * @param minZoom minimum zoom value this Composable can possess
70+ * @param maxZoom maximum zoom value this Composable can possess
71+ * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
72+ * empty space on sides or edges of parent
73+ * @param zoomEnabled when set to true zoom is enabled
74+ * @param panEnabled when set to true pan is enabled
75+ * @param rotationEnabled when set to true rotation is enabled
1676 */
1777@Composable
1878fun rememberZoomState (
1979 initialZoom : Float = 1f,
2080 initialRotation : Float = 0f,
2181 minZoom : Float = 1f,
22- maxZoom : Float = 5f
82+ maxZoom : Float = 5f,
83+ zoomEnabled : Boolean = true,
84+ panEnabled : Boolean = true,
85+ rotationEnabled : Boolean = false,
86+ limitPan : Boolean = false,
87+ key1 : Any? ,
88+ key2 : Any? ,
2389): ZoomState {
24- return remember {
90+ return remember(key1, key2) {
2591 ZoomState (
2692 initialZoom = initialZoom,
2793 initialRotation = initialRotation,
2894 minZoom = minZoom,
29- maxZoom = maxZoom
95+ maxZoom = maxZoom,
96+ zoomEnabled = zoomEnabled,
97+ panEnabled = panEnabled,
98+ rotationEnabled = rotationEnabled,
99+ limitPan = limitPan
100+ )
101+ }
102+ }
103+
104+ /* *
105+ * * Create and [remember] the [ZoomState] based on the currently appropriate transform
106+ * configuration to allow changing pan, zoom, and rotation.
107+ *
108+ * @param initialZoom zoom set initially
109+ * @param initialRotation rotation set initially
110+ * @param minZoom minimum zoom value this Composable can possess
111+ * @param maxZoom maximum zoom value this Composable can possess
112+ * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
113+ * empty space on sides or edges of parent
114+ * @param zoomEnabled when set to true zoom is enabled
115+ * @param panEnabled when set to true pan is enabled
116+ * @param rotationEnabled when set to true rotation is enabled
117+ * @param keys are used to reset remember block to initial calculations. This can be used
118+ * when image, contentScale or any property changes which requires values to be reset to initial
119+ * values
120+ */
121+ @Composable
122+ fun rememberZoomState (
123+ initialZoom : Float = 1f,
124+ initialRotation : Float = 0f,
125+ minZoom : Float = 1f,
126+ maxZoom : Float = 5f,
127+ zoomEnabled : Boolean = true,
128+ panEnabled : Boolean = true,
129+ rotationEnabled : Boolean = false,
130+ limitPan : Boolean = false,
131+ vararg keys : Any?
132+ ): ZoomState {
133+ return remember(keys) {
134+ ZoomState (
135+ initialZoom = initialZoom,
136+ initialRotation = initialRotation,
137+ minZoom = minZoom,
138+ maxZoom = maxZoom,
139+ zoomEnabled = zoomEnabled,
140+ panEnabled = panEnabled,
141+ rotationEnabled = rotationEnabled,
142+ limitPan = limitPan
30143 )
31144 }
32145}
@@ -35,13 +148,23 @@ fun rememberZoomState(
35148 * * State of the zoom. Allows the developer to change zoom, pan, translate,
36149 * or get current state by
37150 * calling methods on this object. To be hosted and passed to [Modifier.zoom]
151+ * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
152+ * empty space on sides or edges of parent.
153+
154+ * @param zoomEnabled when set to true zoom is enabled
155+ * @param panEnabled when set to true pan is enabled
156+ * @param rotationEnabled when set to true rotation is enabled
38157 */
39158@Immutable
40159open class ZoomState internal constructor(
41160 initialZoom : Float = 1f ,
42161 initialRotation : Float = 0f ,
43162 minZoom : Float = 1f ,
44- maxZoom : Float = 5f
163+ maxZoom : Float = 5f ,
164+ private val zoomEnabled : Boolean = true ,
165+ private val panEnabled : Boolean = true ,
166+ internal val rotationEnabled : Boolean = true ,
167+ internal val limitPan : Boolean = false
45168) {
46169
47170 internal val zoomMin = minZoom.coerceAtLeast(.5f )
@@ -74,11 +197,51 @@ open class ZoomState internal constructor(
74197 )
75198
76199
77- fun boundPan (maxX : Float , maxY : Float ) {
78- animatablePan.updateBounds(
79- Offset (- maxX, - maxY),
80- Offset (maxX, maxY)
81- )
200+ open fun boundPan (lowerBound : Offset , upperBound : Offset ) {
201+ animatablePan.updateBounds(lowerBound, upperBound)
202+ }
203+
204+ internal open suspend fun updateZoomState (
205+ size : IntSize ,
206+ gesturePan : Offset ,
207+ gestureZoom : Float ,
208+ gestureRotate : Float ,
209+ ) {
210+ var zoom = zoom
211+
212+ val boundPan = limitPan && ! rotationEnabled
213+
214+ val rotation = if (rotationEnabled) {
215+ rotation + gestureRotate
216+ } else {
217+ 0f
218+ }
219+
220+ if (panEnabled) {
221+ val offset = pan
222+ var newOffset = offset + gesturePan.times(zoom)
223+
224+ if (boundPan) {
225+ val maxX = (size.width * (zoom - 1 ) / 2f )
226+ .coerceAtLeast(0f )
227+ val maxY = (size.height * (zoom - 1 ) / 2f )
228+ .coerceAtLeast(0f )
229+ newOffset = Offset (
230+ newOffset.x.coerceIn(- maxX, maxX),
231+ newOffset.y.coerceIn(- maxY, maxY)
232+ )
233+ }
234+ snapPanTo(newOffset)
235+ }
236+
237+ if (zoomEnabled) {
238+ zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
239+ snapZoomTo(zoom)
240+ }
241+
242+ if (rotationEnabled) {
243+ snapRotationTo(rotation)
244+ }
82245 }
83246
84247 internal suspend fun animatePanTo (pan : Offset ) = coroutineScope {
@@ -94,14 +257,20 @@ open class ZoomState internal constructor(
94257 }
95258
96259 internal suspend fun snapPanTo (offset : Offset ) = coroutineScope {
97- animatablePan.snapTo(offset)
260+ if (panEnabled) {
261+ animatablePan.snapTo(offset)
262+ }
98263 }
99264
100265 internal suspend fun snapZoomTo (zoom : Float ) = coroutineScope {
101- animatableZoom.snapTo(zoom)
266+ if (zoomEnabled) {
267+ animatableZoom.snapTo(zoom)
268+ }
102269 }
103270
104271 internal suspend fun snapRotationTo (rotation : Float ) = coroutineScope {
105- animatableRotation.snapTo(rotation)
272+ if (rotationEnabled) {
273+ animatableRotation.snapTo(rotation)
274+ }
106275 }
107276}
0 commit comments