11package com.smarttoolfactory.image.zoom
22
3- import androidx.compose.animation.core.exponentialDecay
4- import androidx.compose.runtime.*
5- import androidx.compose.ui.Modifier
6- import androidx.compose.ui.geometry.Offset
7- import androidx.compose.ui.geometry.Rect
8- import androidx.compose.ui.geometry.Size
9- import androidx.compose.ui.input.pointer.PointerInputChange
10- import androidx.compose.ui.input.pointer.util.VelocityTracker
3+ import androidx.compose.runtime.Composable
4+ import androidx.compose.runtime.remember
115import androidx.compose.ui.unit.IntSize
12- import kotlinx.coroutines.coroutineScope
13-
146
157/* *
168 * * Create and [remember] the [ZoomState] based on the currently appropriate transform
@@ -32,7 +24,8 @@ import kotlinx.coroutines.coroutineScope
3224 */
3325@Composable
3426fun rememberEnhancedZoomState (
35- size : IntSize ,
27+ imageSize : IntSize ,
28+ containerSize : IntSize ,
3629 initialZoom : Float = 1f,
3730 initialRotation : Float = 0f,
3831 minZoom : Float = 1f,
@@ -45,9 +38,9 @@ fun rememberEnhancedZoomState(
4538): EnhancedZoomState {
4639 return remember(key1) {
4740 EnhancedZoomState (
48- size = size,
41+ imageSize = imageSize,
42+ containerSize = containerSize,
4943 initialZoom = initialZoom,
50- initialRotation = initialRotation,
5144 minZoom = minZoom,
5245 maxZoom = maxZoom,
5346 zoomEnabled = zoomEnabled,
@@ -67,7 +60,6 @@ fun rememberEnhancedZoomState(
6760 * values
6861 *
6962 * @param initialZoom zoom set initially
70- * @param initialRotation rotation set initially
7163 * @param minZoom minimum zoom value this Composable can possess
7264 * @param maxZoom maximum zoom value this Composable can possess
7365 * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
@@ -78,9 +70,9 @@ fun rememberEnhancedZoomState(
7870 */
7971@Composable
8072fun rememberEnhancedZoomState (
81- size : IntSize ,
73+ imageSize : IntSize ,
74+ containerSize : IntSize ,
8275 initialZoom : Float = 1f,
83- initialRotation : Float = 0f,
8476 minZoom : Float = 1f,
8577 maxZoom : Float = 5f,
8678 zoomEnabled : Boolean = true,
@@ -92,9 +84,9 @@ fun rememberEnhancedZoomState(
9284): EnhancedZoomState {
9385 return remember(key1, key2) {
9486 EnhancedZoomState (
95- size = size,
87+ imageSize = imageSize,
88+ containerSize = containerSize,
9689 initialZoom = initialZoom,
97- initialRotation = initialRotation,
9890 minZoom = minZoom,
9991 maxZoom = maxZoom,
10092 zoomEnabled = zoomEnabled,
@@ -110,7 +102,6 @@ fun rememberEnhancedZoomState(
110102 * configuration to allow changing pan, zoom, and rotation.
111103 *
112104 * @param initialZoom zoom set initially
113- * @param initialRotation rotation set initially
114105 * @param minZoom minimum zoom value this Composable can possess
115106 * @param maxZoom maximum zoom value this Composable can possess
116107 * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
@@ -124,9 +115,9 @@ fun rememberEnhancedZoomState(
124115 */
125116@Composable
126117fun rememberEnhancedZoomState (
127- size : IntSize ,
118+ imageSize : IntSize ,
119+ containerSize : IntSize ,
128120 initialZoom : Float = 1f,
129- initialRotation : Float = 0f,
130121 minZoom : Float = 1f,
131122 maxZoom : Float = 5f,
132123 zoomEnabled : Boolean = true,
@@ -137,9 +128,9 @@ fun rememberEnhancedZoomState(
137128): EnhancedZoomState {
138129 return remember(keys) {
139130 EnhancedZoomState (
140- size = size,
131+ imageSize = imageSize,
132+ containerSize = containerSize,
141133 initialZoom = initialZoom,
142- initialRotation = initialRotation,
143134 minZoom = minZoom,
144135 maxZoom = maxZoom,
145136 zoomEnabled = zoomEnabled,
@@ -148,227 +139,4 @@ fun rememberEnhancedZoomState(
148139 limitPan = limitPan
149140 )
150141 }
151- }
152-
153- /* *
154- * * State of the zoom. Allows the developer to change zoom, pan, translate,
155- * or get current state by
156- * calling methods on this object. To be hosted and passed to [Modifier.zoom]
157- * @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
158- * empty space on sides or edges of parent.
159-
160- * @param zoomEnabled when set to true zoom is enabled
161- * @param panEnabled when set to true pan is enabled
162- * @param rotationEnabled when set to true rotation is enabled
163- */
164- open class EnhancedZoomState constructor(
165- var size : IntSize ,
166- initialZoom : Float = 1f ,
167- initialRotation : Float = 0f ,
168- minZoom : Float = .5f ,
169- maxZoom : Float = 5f ,
170- override var zoomEnabled : Boolean = true ,
171- override var panEnabled : Boolean = true ,
172- override var rotationEnabled : Boolean = true ,
173- override var limitPan : Boolean = false
174- ) : ZoomState(
175- initialZoom, initialRotation, minZoom, maxZoom, zoomEnabled
176- ) {
177-
178- val isZooming = animatableZoom.isRunning
179- val isPanning = animatablePan.isRunning
180- val isRotating = animatableRotation.isRunning
181-
182- val isAnimationRunning = isZooming || isPanning || isRotating
183-
184- var rectDraw =
185- Rect (offset = Offset .Zero , size = Size (size.width.toFloat(), size.height.toFloat()))
186- var rectCrop by mutableStateOf(rectDraw.copy())
187-
188- private val velocityTracker = VelocityTracker ()
189-
190- val enhancedZoomData: EnhancedZoomData
191- get() = EnhancedZoomData (
192- zoom = animatableZoom.targetValue,
193- pan = animatablePan.targetValue,
194- rotation = animatableRotation.targetValue,
195- drawRect = rectDraw,
196- cropRect = rectCrop
197- )
198-
199- private fun getBounds (): Offset {
200- return getBounds(size)
201- }
202-
203- // Touch gestures
204- open suspend fun onDown (change : PointerInputChange ) {}
205-
206- open suspend fun onMove (change : PointerInputChange ) {}
207-
208- open suspend fun onUp (change : PointerInputChange ) {}
209-
210- // Transform Gestures
211- internal open suspend fun onGestureStart (change : PointerInputChange ) {}
212-
213- internal open suspend fun onGesture (
214- centroid : Offset ,
215- pan : Offset ,
216- zoom : Float ,
217- rotation : Float ,
218- mainPointer : PointerInputChange ,
219- changes : List <PointerInputChange >
220- ) = coroutineScope {
221-
222- updateZoomState(
223- size = size,
224- gestureZoom = zoom,
225- gesturePan = pan,
226- gestureRotate = rotation
227- )
228-
229- // Fling Gesture
230- if (changes.size == 1 ) {
231- addPosition(
232- mainPointer.uptimeMillis,
233- mainPointer.position
234- )
235- }
236- }
237-
238- internal suspend fun onGestureEnd (onAnimationEnd : () -> Unit ) {
239- if (zoom > 1 ) {
240- fling()
241- }
242- resetToValidBounds()
243- onAnimationEnd()
244- }
245-
246- // Double Tap
247- internal suspend fun onDoubleTap (onAnimationEnd : () -> Unit ) {
248- resetTracking()
249- resetWithAnimation()
250- onAnimationEnd()
251- }
252-
253- /* *
254- * Resets to bounds with animation and resets tracking for fling animation
255- */
256- private suspend fun resetToValidBounds () {
257- val zoom = zoom.coerceAtLeast(1f )
258- val bounds = getBounds()
259-
260- val pan = Offset (
261- pan.x.coerceIn(- bounds.x, bounds.x),
262- pan.y.coerceIn(- bounds.y, bounds.y)
263- )
264-
265- resetWithAnimation(pan = pan, zoom = zoom)
266- resetTracking()
267- }
268-
269-
270- // Fling gesture
271- private fun addPosition (timeMillis : Long , position : Offset ) {
272- velocityTracker.addPosition(
273- timeMillis = timeMillis,
274- position = position
275- )
276- }
277-
278- private suspend fun fling () {
279- val velocityTracker = velocityTracker.calculateVelocity()
280- val velocity = Offset (velocityTracker.x, velocityTracker.y)
281-
282- animatablePan.animateDecay(
283- velocity,
284- exponentialDecay(
285- absVelocityThreshold = 20f
286- )
287- )
288- }
289-
290- private fun resetTracking () {
291- velocityTracker.resetTracking()
292- }
293-
294- override suspend fun updateZoomState (
295- size : IntSize ,
296- gesturePan : Offset ,
297- gestureZoom : Float ,
298- gestureRotate : Float ,
299- ) {
300- val zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
301- val rotation = if (rotationEnabled) {
302- rotation + gestureRotate
303- } else {
304- 0f
305- }
306-
307- if (panEnabled) {
308- val newOffset = pan + gesturePan.times(zoom)
309- snapPanTo(newOffset)
310- }
311-
312- if (zoomEnabled) {
313- snapZoomTo(zoom)
314- }
315-
316- if (rotationEnabled) {
317- snapRotationTo(rotation)
318- }
319-
320- val width = size.width
321- val height = size.height
322- val offsetX = (width * (zoom - 1 ) / 2f ).coerceAtLeast(0f ) - pan.x
323- val offsetY = (height * (zoom - 1 ) / 2f ).coerceAtLeast(0f ) - pan.y
324-
325- rectCrop = getCropRect(
326- bitmapWidth = 1024 ,
327- bitmapHeight = 768 ,
328- imageWidth = width.toFloat(),
329- imageHeight = height.toFloat(),
330- pan = Offset (
331- x = offsetX / zoom,
332- y = offsetY / zoom,
333- ),
334- zoom = zoom,
335- rectBounds = rectDraw
336- )
337- println (
338- " 🔥 EnhancedZoomState updateZoomState()\n " +
339- " offsetX: $offsetX , offsetY: $offsetY " +
340- " size: $size , pan: $pan , zoom: $zoom \n " +
341- " rectCrop: $rectCrop "
342- )
343- }
344- }
345-
346- /* *
347- * Get rectangle of current transformation of [pan], [zoom] and current bounds of the Composable's
348- * selected area as [rectBounds]
349- */
350- fun getCropRect (
351- bitmapWidth : Int ,
352- bitmapHeight : Int ,
353- imageWidth : Float ,
354- imageHeight : Float ,
355- pan : Offset ,
356- zoom : Float ,
357- rectBounds : Rect
358- ): Rect {
359- val widthRatio = bitmapWidth / imageWidth
360- val heightRatio = bitmapHeight / imageHeight
361-
362- val width = (widthRatio * rectBounds.width / zoom).coerceIn(0f , bitmapWidth.toFloat())
363- val height = (heightRatio * rectBounds.height / zoom).coerceIn(0f , bitmapHeight.toFloat())
364-
365- val offsetXInBitmap = (widthRatio * (pan.x + rectBounds.left / zoom))
366- .coerceIn(0f , bitmapWidth - width)
367- val offsetYInBitmap = heightRatio * (pan.y + rectBounds.top / zoom)
368- .coerceIn(0f , bitmapHeight - height)
369-
370- return Rect (
371- offset = Offset (offsetXInBitmap, offsetYInBitmap),
372- size = Size (width, height)
373- )
374142}
0 commit comments