Skip to content

Commit 4ef269a

Browse files
Merge pull request #19 from Nimrodda/fixed-aspect-ratio
Added an option to enable fixed cropping aspect ratio
2 parents 28a3506 + 8733d2f commit 4ef269a

File tree

9 files changed

+112
-32
lines changed

9 files changed

+112
-32
lines changed

app/src/main/java/com/smarttoolfactory/composecropper/preferences/CropPropertySelection.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ internal fun CropPropertySelectionMenu(
6363
}
6464
)
6565

66+
Title("Fix aspect ratio")
67+
FixedAspectRatioEnableSelection(
68+
fixedAspectRatioEnabled = cropProperties.fixedAspectRatio,
69+
onFixedAspectRatioChanged = {
70+
onCropPropertiesChange(
71+
cropProperties.copy(fixedAspectRatio = it)
72+
)
73+
}
74+
)
75+
6676
Title("Frame")
6777
CropFrameSelection(
6878
aspectRatio = aspectRatio,
@@ -193,6 +203,18 @@ internal fun FlingEnableSelection(
193203

194204
}
195205

206+
@Composable
207+
internal fun FixedAspectRatioEnableSelection(
208+
fixedAspectRatioEnabled: Boolean,
209+
onFixedAspectRatioChanged: (Boolean) -> Unit
210+
) {
211+
FullRowSwitch(
212+
label = "Enable fixed aspect ratio",
213+
state = fixedAspectRatioEnabled,
214+
onStateChange = onFixedAspectRatioChanged
215+
)
216+
}
217+
196218
@Composable
197219
internal fun PanEnableSelection(
198220
panEnabled: Boolean,

cropper/src/main/java/com/smarttoolfactory/cropper/ImageCropper.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,20 @@ fun ImageCropper(
9696

9797
val cropType = cropProperties.cropType
9898
val contentScale = cropProperties.contentScale
99-
99+
val fixedAspectRatio = cropProperties.fixedAspectRatio
100100
val cropOutline = cropProperties.cropOutlineProperty.cropOutline
101101

102102
// these keys are for resetting cropper when image width/height, contentScale or
103103
// overlay aspect ratio changes
104104
val resetKeys =
105-
getResetKeys(scaledImageBitmap, imageWidthPx, imageHeightPx, contentScale, cropType)
105+
getResetKeys(
106+
scaledImageBitmap,
107+
imageWidthPx,
108+
imageHeightPx,
109+
contentScale,
110+
cropType,
111+
fixedAspectRatio
112+
)
106113

107114
val cropState = rememberCropState(
108115
imageSize = IntSize(bitmapWidth, bitmapHeight),
@@ -348,19 +355,22 @@ private fun getResetKeys(
348355
imageWidthPx: Int,
349356
imageHeightPx: Int,
350357
contentScale: ContentScale,
351-
cropType: CropType
358+
cropType: CropType,
359+
fixedAspectRatio: Boolean,
352360
) = remember(
353361
scaledImageBitmap,
354362
imageWidthPx,
355363
imageHeightPx,
356364
contentScale,
357-
cropType
365+
cropType,
366+
fixedAspectRatio,
358367
) {
359368
arrayOf(
360369
scaledImageBitmap,
361370
imageWidthPx,
362371
imageHeightPx,
363372
contentScale,
364-
cropType
373+
cropType,
374+
fixedAspectRatio,
365375
)
366376
}

cropper/src/main/java/com/smarttoolfactory/cropper/model/AspectRatios.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ val aspectRatios = listOf(
1818
),
1919
CropAspectRatio(
2020
title = "Original",
21-
shape = createRectShape(AspectRatio.Unspecified),
22-
aspectRatio = AspectRatio.Unspecified
21+
shape = createRectShape(AspectRatio.Original),
22+
aspectRatio = AspectRatio.Original
2323
),
2424
CropAspectRatio(
2525
title = "1:1",

cropper/src/main/java/com/smarttoolfactory/cropper/model/CropAspectRatio.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ import androidx.compose.ui.graphics.Shape
1111
data class CropAspectRatio(
1212
val title: String,
1313
val shape: Shape,
14-
val aspectRatio: AspectRatio = AspectRatio.Unspecified,
14+
val aspectRatio: AspectRatio = AspectRatio.Original,
1515
val icons: List<Int> = listOf()
1616
)
1717

1818
/**
1919
* Value class for containing aspect ratio
20-
* and [AspectRatio.Unspecified] for comparing
20+
* and [AspectRatio.Original] for comparing
2121
*/
2222
@Immutable
2323
data class AspectRatio(val value: Float) {
2424
companion object {
25-
val Unspecified = AspectRatio(-1f)
25+
val Original = AspectRatio(-1f)
2626
}
2727
}

cropper/src/main/java/com/smarttoolfactory/cropper/settings/CropDefaults.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ object CropDefaults {
3333
contentScale: ContentScale = ContentScale.Fit,
3434
cropOutlineProperty: CropOutlineProperty,
3535
aspectRatio: AspectRatio = aspectRatios[2].aspectRatio,
36-
overlayRatio:Float = .9f,
36+
overlayRatio: Float = .9f,
3737
pannable: Boolean = true,
3838
fling: Boolean = false,
3939
zoomable: Boolean = true,
4040
rotatable: Boolean = false,
41-
requiredSize: IntSize? = null,
41+
fixedAspectRatio: Boolean = false,
42+
requiredSize: IntSize? = null
4243
): CropProperties {
4344
return CropProperties(
4445
cropType = cropType,
@@ -52,7 +53,8 @@ object CropDefaults {
5253
fling = fling,
5354
zoomable = zoomable,
5455
rotatable = rotatable,
55-
requiredSize = requiredSize,
56+
fixedAspectRatio = fixedAspectRatio,
57+
requiredSize = requiredSize
5658
)
5759
}
5860

@@ -97,7 +99,8 @@ data class CropProperties internal constructor(
9799
val rotatable: Boolean,
98100
val zoomable: Boolean,
99101
val maxZoom: Float,
100-
val requiredSize: IntSize? = null,
102+
val fixedAspectRatio: Boolean = false,
103+
val requiredSize: IntSize? = null
101104
)
102105

103106
/**

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fun rememberCropState(
3838
val zoomable = cropProperties.zoomable
3939
val pannable = cropProperties.pannable
4040
val rotatable = cropProperties.rotatable
41-
41+
val fixedAspectRatio = cropProperties.fixedAspectRatio
4242

4343
return remember(*keys) {
4444
when (cropType) {
@@ -72,7 +72,8 @@ fun rememberCropState(
7272
zoomable = zoomable,
7373
pannable = pannable,
7474
rotatable = rotatable,
75-
limitPan = true
75+
limitPan = true,
76+
fixedAspectRatio = fixedAspectRatio,
7677
)
7778
}
7879
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ abstract class CropState internal constructor(
7474
containerSize.width.toFloat(),
7575
containerSize.height.toFloat(),
7676
drawAreaSize.width.toFloat(),
77-
drawAreaSize.height.toFloat(),
7877
aspectRatio,
7978
overlayRatio
8079
),
@@ -158,7 +157,6 @@ abstract class CropState internal constructor(
158157
containerSize.width.toFloat(),
159158
containerSize.height.toFloat(),
160159
drawAreaSize.width.toFloat(),
161-
drawAreaSize.height.toFloat(),
162160
aspectRatio,
163161
overlayRatio
164162
)
@@ -398,16 +396,17 @@ abstract class CropState internal constructor(
398396
containerWidth: Float,
399397
containerHeight: Float,
400398
drawAreaWidth: Float,
401-
drawAreaHeight: Float,
402399
aspectRatio: AspectRatio,
403400
coefficient: Float
404401
): Rect {
405402

406-
if (aspectRatio == AspectRatio.Unspecified) {
403+
if (aspectRatio == AspectRatio.Original) {
404+
val imageAspectRatio = imageSize.width.toFloat() / imageSize.height.toFloat()
407405

408406
// Maximum width and height overlay rectangle can be measured with
409407
val overlayWidthMax = drawAreaWidth.coerceAtMost(containerWidth * coefficient)
410-
val overlayHeightMax = drawAreaHeight.coerceAtMost(containerHeight * coefficient)
408+
val overlayHeightMax =
409+
(overlayWidthMax / imageAspectRatio).coerceAtMost(containerHeight * coefficient)
411410

412411
val offsetX = (containerWidth - overlayWidthMax) / 2f
413412
val offsetY = (containerHeight - overlayHeightMax) / 2f

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import kotlinx.coroutines.coroutineScope
2828
* @param pannable when set to true pan is enabled
2929
* @param rotatable when set to true rotation is enabled
3030
* @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
31+
* @param fixedAspectRatio when set to true aspect ratio of overlay is fixed
3132
* empty space on sides or edges of parent
3233
*/
3334
class DynamicCropState internal constructor(
@@ -43,7 +44,8 @@ class DynamicCropState internal constructor(
4344
zoomable: Boolean,
4445
pannable: Boolean,
4546
rotatable: Boolean,
46-
limitPan: Boolean
47+
limitPan: Boolean,
48+
private val fixedAspectRatio: Boolean,
4749
) : CropState(
4850
imageSize = imageSize,
4951
containerSize = containerSize,
@@ -86,7 +88,7 @@ class DynamicCropState internal constructor(
8688
override suspend fun updateProperties(cropProperties: CropProperties, forceUpdate: Boolean) {
8789
handleSize = cropProperties.handleSize
8890
minOverlaySize = handleSize * 2
89-
91+
9092
super.updateProperties(cropProperties, forceUpdate)
9193
}
9294

@@ -137,13 +139,23 @@ class DynamicCropState internal constructor(
137139
minDimension = minOverlaySize,
138140
rectTemp = rectTemp,
139141
overlayRect = overlayRect,
140-
change = change
142+
change = change,
143+
aspectRatio = getAspectRatio(),
144+
fixedAspectRatio = fixedAspectRatio,
141145
)
142146

143147
snapOverlayRectTo(newRect)
144148
}
145149
}
146150

151+
private fun getAspectRatio(): Float {
152+
return if (aspectRatio == AspectRatio.Original) {
153+
imageSize.width / imageSize.height.toFloat()
154+
} else {
155+
aspectRatio.value
156+
}
157+
}
158+
147159
override suspend fun onUp(change: PointerInputChange) = coroutineScope {
148160
if (touchRegion != TouchRegion.None) {
149161

@@ -253,7 +265,6 @@ class DynamicCropState internal constructor(
253265
containerSize.width.toFloat(),
254266
containerSize.height.toFloat(),
255267
drawAreaSize.width.toFloat(),
256-
drawAreaSize.height.toFloat(),
257268
aspectRatio,
258269
overlayRatio
259270
)
@@ -351,7 +362,9 @@ class DynamicCropState internal constructor(
351362
minDimension: Float,
352363
rectTemp: Rect,
353364
overlayRect: Rect,
354-
change: PointerInputChange
365+
change: PointerInputChange,
366+
aspectRatio: Float,
367+
fixedAspectRatio: Boolean,
355368
): Rect {
356369

357370
val position = change.position
@@ -368,7 +381,15 @@ class DynamicCropState internal constructor(
368381
// Set position of top left while moving with top left handle and
369382
// limit position to not intersect other handles
370383
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
371-
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
384+
val top = if (fixedAspectRatio) {
385+
// If aspect ratio is fixed we need to calculate top position based on
386+
// left position and aspect ratio
387+
val width = rectTemp.right - left
388+
val height = width / aspectRatio
389+
rectTemp.bottom - height
390+
} else {
391+
screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
392+
}
372393
Rect(
373394
left = left,
374395
top = top,
@@ -382,7 +403,15 @@ class DynamicCropState internal constructor(
382403
// Set position of top left while moving with bottom left handle and
383404
// limit position to not intersect other handles
384405
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
385-
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
406+
val bottom = if (fixedAspectRatio) {
407+
// If aspect ratio is fixed we need to calculate bottom position based on
408+
// left position and aspect ratio
409+
val width = rectTemp.right - left
410+
val height = width / aspectRatio
411+
rectTemp.top + height
412+
} else {
413+
screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
414+
}
386415
Rect(
387416
left = left,
388417
top = rectTemp.top,
@@ -396,7 +425,15 @@ class DynamicCropState internal constructor(
396425
// Set position of top left while moving with top right handle and
397426
// limit position to not intersect other handles
398427
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
399-
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
428+
val top = if (fixedAspectRatio) {
429+
// If aspect ratio is fixed we need to calculate top position based on
430+
// right position and aspect ratio
431+
val width = right - rectTemp.left
432+
val height = width / aspectRatio
433+
rectTemp.bottom - height
434+
} else {
435+
screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
436+
}
400437

401438
Rect(
402439
left = rectTemp.left,
@@ -412,7 +449,15 @@ class DynamicCropState internal constructor(
412449
// Set position of top left while moving with bottom right handle and
413450
// limit position to not intersect other handles
414451
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
415-
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
452+
val bottom = if (fixedAspectRatio) {
453+
// If aspect ratio is fixed we need to calculate bottom position based on
454+
// right position and aspect ratio
455+
val width = right - rectTemp.left
456+
val height = width / aspectRatio
457+
rectTemp.top + height
458+
} else {
459+
screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
460+
}
416461

417462
Rect(
418463
left = rectTemp.left,

cropper/src/main/java/com/smarttoolfactory/cropper/util/ShapeUtils.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ fun createRectShape(aspectRatio: AspectRatio): GenericShape {
8181
val width = size.width
8282
val height = size.height
8383
val shapeSize =
84-
if (aspectRatio == AspectRatio.Unspecified) Size(width, height)
84+
if (aspectRatio == AspectRatio.Original) Size(width, height)
8585
else if (value > 1) Size(width = width, height = width / value)
8686
else Size(width = height * value, height = height)
8787

@@ -154,7 +154,7 @@ fun calculateSizeAndOffsetFromAspectRatio(
154154

155155
val value = aspectRatio.value
156156

157-
val newSize = if (aspectRatio == AspectRatio.Unspecified) {
157+
val newSize = if (aspectRatio == AspectRatio.Original) {
158158
Size(width * coefficient, height * coefficient)
159159
} else if (value > 1) {
160160
Size(

0 commit comments

Comments
 (0)