Skip to content

Commit 445ae96

Browse files
fix aspect ratio changes
* animates overlay rect into bounds after isRectInContainerBounds check * adds animation spec to Animatable animations * fix aspect ratio calculation and assign current value to a field
1 parent 1bc436d commit 445ae96

1 file changed

Lines changed: 104 additions & 30 deletions

File tree

cropper/src/main/java/com/smarttoolfactory/cropper/state/CropStateImpl.kt

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.smarttoolfactory.cropper.state
22

33
import androidx.compose.animation.core.Animatable
4+
import androidx.compose.animation.core.AnimationSpec
45
import androidx.compose.animation.core.VectorConverter
56
import androidx.compose.animation.core.tween
67
import 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

Comments
 (0)