11package com.smarttoolfactory.cropper.state
22
33import androidx.compose.animation.core.Animatable
4+ import androidx.compose.animation.core.AnimationSpec
45import androidx.compose.animation.core.VectorConverter
56import androidx.compose.animation.core.tween
67import androidx.compose.ui.geometry.Offset
@@ -43,7 +44,7 @@ abstract class CropState internal constructor(
4344 imageSize : IntSize ,
4445 containerSize : IntSize ,
4546 drawAreaSize : IntSize ,
46- private var aspectRatio : AspectRatio ,
47+ internal var aspectRatio : AspectRatio ,
4748 maxZoom : Float ,
4849 var fling : Boolean = true ,
4950 zoomable : Boolean = true ,
@@ -67,8 +68,8 @@ abstract class CropState internal constructor(
6768 getOverlayFromAspectRatio(
6869 containerSize.width.toFloat(),
6970 containerSize.height.toFloat(),
70- drawAreaRect.size. width,
71- drawAreaRect.size. height,
71+ drawAreaSize. width.toFloat() ,
72+ drawAreaSize. height.toFloat() ,
7273 aspectRatio
7374 ),
7475 Rect .VectorConverter
@@ -87,10 +88,22 @@ abstract class CropState internal constructor(
8788 private set
8889
8990
91+ private var initialized: Boolean = false
92+
93+ internal suspend fun init () {
94+ // When initial aspect ratio doesn't match drawable area
95+ // overlay gets updated so update draw area as well
96+ animateTransformationToOverlayBounds(overlayRect, animate = true )
97+ initialized = true
98+ }
99+
90100 /* *
91101 * Update properties of [CropState] and animate to valid intervals if required
92102 */
93103 internal open suspend fun updateProperties (cropProperties : CropProperties ) {
104+
105+ if (! initialized) return
106+
94107 fling = cropProperties.fling
95108 pannable = cropProperties.pannable
96109 zoomable = cropProperties.zoomable
@@ -102,14 +115,21 @@ abstract class CropState internal constructor(
102115 val aspectRatio = cropProperties.aspectRatio
103116
104117 if (this .aspectRatio.value != aspectRatio.value || maxZoom != zoomMax) {
118+ this .aspectRatio = aspectRatio
105119
106120 zoomMax = maxZoom
107121 animatableZoom.updateBounds(zoomMin, zoomMax)
108122
109- val currentZoom = if (zoom> zoomMax) zoomMax else zoom
123+ val currentZoom = if (zoom > zoomMax) zoomMax else zoom
124+
125+ // Set new zoom
126+ snapZoomTo(currentZoom)
110127
111- resetWithAnimation(zoom = currentZoom)
128+ // Calculate new region of image is drawn. It can be drawn left of 0 and right
129+ // of container width depending on transformation
130+ drawAreaRect = updateImageDrawRectFromTransformation()
112131
132+ // Update overlay rectangle based on current draw area and new aspect ratio
113133 animateOverlayRectTo(
114134 getOverlayFromAspectRatio(
115135 containerSize.width.toFloat(),
@@ -121,17 +141,21 @@ abstract class CropState internal constructor(
121141 )
122142 }
123143
124- // Update image draw area
125- drawAreaRect = updateImageDrawRectFromTransformation()
144+ // Animate zoom, pan, rotation to move draw area to cover overlay rect
145+ // inside draw area rect
146+ animateTransformationToOverlayBounds(overlayRect, animate = true )
126147 }
127148
128149 /* *
129150 * Animate overlay rectangle to target value
130151 */
131- internal suspend fun animateOverlayRectTo (rect : Rect ) {
152+ internal suspend fun animateOverlayRectTo (
153+ rect : Rect ,
154+ animationSpec : AnimationSpec <Rect > = tween(400)
155+ ) {
132156 animatableRectOverlay.animateTo(
133157 targetValue = rect,
134- animationSpec = tween( 400 )
158+ animationSpec = animationSpec
135159 )
136160 }
137161
@@ -169,12 +193,31 @@ abstract class CropState internal constructor(
169193
170194 // Double Tap
171195 internal abstract suspend fun onDoubleTap (
172- pan : Offset = Offset . Zero ,
196+ offset : Offset ,
173197 zoom : Float = 1f,
174- rotation : Float = 0f,
175198 onAnimationEnd : () -> Unit
176199 )
177200
201+ /* *
202+ * Check if area that image is drawn covers [overlayRect]
203+ */
204+ internal fun isOverlayInImageDrawBounds (): Boolean {
205+ return drawAreaRect.left <= overlayRect.left &&
206+ drawAreaRect.top <= overlayRect.top &&
207+ drawAreaRect.right >= overlayRect.right &&
208+ drawAreaRect.bottom >= overlayRect.bottom
209+ }
210+
211+ /* *
212+ * Check if [rect] is inside container bounds
213+ */
214+ internal fun isRectInContainerBounds (rect : Rect ): Boolean {
215+ return rect.left >= 0 &&
216+ rect.right <= containerSize.width &&
217+ rect.top >= 0 &&
218+ rect.bottom <= containerSize.height
219+ }
220+
178221 /* *
179222 * Update rectangle for area that image is drawn. This rect changes when zoom and
180223 * pan changes and position of image changes on screen as result of transformation.
@@ -223,7 +266,11 @@ abstract class CropState internal constructor(
223266 * Resets to bounds with animation and resets tracking for fling animation.
224267 * Changes pan, zoom and rotation to valid bounds based on [drawAreaRect] and [overlayRect]
225268 */
226- internal suspend fun animateTransformationToOverlayBounds () {
269+ internal suspend fun animateTransformationToOverlayBounds (
270+ overlayRect : Rect ,
271+ animate : Boolean ,
272+ animationSpec : AnimationSpec <Float > = tween(400)
273+ ) {
227274
228275 val zoom = zoom.coerceAtLeast(1f )
229276
@@ -248,7 +295,18 @@ abstract class CropState internal constructor(
248295 // Update draw area based on new pan and zoom values
249296 drawAreaRect = newDrawAreaRect
250297
251- resetWithAnimation(pan = Offset (newPanX, newPanY), zoom = newZoom)
298+ if (animate) {
299+ resetWithAnimation(
300+ pan = Offset (newPanX, newPanY),
301+ zoom = newZoom,
302+ animationSpec = animationSpec
303+ )
304+ } else {
305+ snapPanXto(newPanX)
306+ snapPanYto(newPanY)
307+ snapZoomTo(newZoom)
308+ }
309+
252310 resetTracking()
253311 }
254312
@@ -258,6 +316,8 @@ abstract class CropState internal constructor(
258316 */
259317 private fun calculateNewZoom (oldRect : Rect , newRect : Rect , zoom : Float ): Float {
260318
319+ if (oldRect.size == Size .Zero || newRect.size == Size .Zero ) return zoom
320+
261321 val widthChange = (newRect.width / oldRect.width)
262322 .coerceAtLeast(1f )
263323 val heightChange = (newRect.height / oldRect.height)
@@ -310,38 +370,47 @@ abstract class CropState internal constructor(
310370 /* *
311371 * Create [Rect] to draw overlay based on selected aspect ratio
312372 */
313- private fun getOverlayFromAspectRatio (
373+ internal fun getOverlayFromAspectRatio (
314374 containerWidth : Float ,
315375 containerHeight : Float ,
316376 drawAreaWidth : Float ,
317377 drawAreaHeight : Float ,
318- aspectRatio : AspectRatio
378+ aspectRatio : AspectRatio ,
379+ coefficient : Float = .9f
319380 ): Rect {
320381
321- val offset = Offset (
322- x = (containerWidth - drawAreaWidth) / 2 ,
323- y = (containerHeight - drawAreaHeight) / 2
324- )
382+ if (aspectRatio == AspectRatio .Unspecified ) {
325383
326- if (aspectRatio == AspectRatio .Unspecified ) return Rect (
327- offset = offset,
328- size = Size (drawAreaWidth, drawAreaHeight)
329- )
384+ // Maximum width and height overlay rectangle can be measured with
385+ val overlayWidthMax = drawAreaWidth.coerceAtMost(containerWidth * coefficient)
386+ val overlayHeightMax = drawAreaHeight.coerceAtMost(containerHeight * coefficient)
387+
388+ val offsetX = (containerWidth - overlayWidthMax) / 2f
389+ val offsetY = (containerHeight - overlayHeightMax) / 2f
390+
391+ return Rect (
392+ offset = Offset (offsetX, offsetY),
393+ size = Size (overlayWidthMax, overlayHeightMax)
394+ )
395+ }
396+
397+ val overlayWidthMax = containerWidth * coefficient
398+ val overlayHeightMax = containerHeight * coefficient
330399
331400 val aspectRatioValue = aspectRatio.value
332401
333- var width = drawAreaWidth
334- var height = drawAreaWidth / aspectRatioValue
402+ var width = overlayWidthMax
403+ var height = overlayWidthMax / aspectRatioValue
335404
336- if (height > drawAreaHeight ) {
337- height = drawAreaHeight
405+ if (height > overlayHeightMax ) {
406+ height = overlayHeightMax
338407 width = height * aspectRatioValue
339408 }
340409
341- val posX = offset.x + ((drawAreaWidth - width) / 2 )
342- val posY = offset.y + ((drawAreaHeight - height) / 2 )
410+ val offsetX = (containerWidth - width) / 2f
411+ val offsetY = (containerHeight - height) / 2f
343412
344- return Rect (offset = Offset (posX, posY ), size = Size (width, height))
413+ return Rect (offset = Offset (offsetX, offsetY ), size = Size (width, height))
345414 }
346415
347416 /* *
@@ -354,6 +423,11 @@ abstract class CropState internal constructor(
354423 overlayRect : Rect
355424 ): Rect {
356425
426+ if (drawAreaRect == Rect .Zero || overlayRect == Rect .Zero ) return Rect (
427+ offset = Offset .Zero ,
428+ Size (bitmapWidth.toFloat(), bitmapHeight.toFloat())
429+ )
430+
357431 // Calculate latest image draw area based on overlay position
358432 // This is valid rectangle that contains crop area inside overlay
359433 val newRect = calculateValidImageDrawRect(overlayRect, drawAreaRect)
0 commit comments