Skip to content

Commit

Permalink
Merge pull request #19 from Nimrodda/fixed-aspect-ratio
Browse files Browse the repository at this point in the history
Added an option to enable fixed cropping aspect ratio
  • Loading branch information
SmartToolFactory authored May 16, 2023
2 parents 28a3506 + 8733d2f commit 4ef269a
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ internal fun CropPropertySelectionMenu(
}
)

Title("Fix aspect ratio")
FixedAspectRatioEnableSelection(
fixedAspectRatioEnabled = cropProperties.fixedAspectRatio,
onFixedAspectRatioChanged = {
onCropPropertiesChange(
cropProperties.copy(fixedAspectRatio = it)
)
}
)

Title("Frame")
CropFrameSelection(
aspectRatio = aspectRatio,
Expand Down Expand Up @@ -193,6 +203,18 @@ internal fun FlingEnableSelection(

}

@Composable
internal fun FixedAspectRatioEnableSelection(
fixedAspectRatioEnabled: Boolean,
onFixedAspectRatioChanged: (Boolean) -> Unit
) {
FullRowSwitch(
label = "Enable fixed aspect ratio",
state = fixedAspectRatioEnabled,
onStateChange = onFixedAspectRatioChanged
)
}

@Composable
internal fun PanEnableSelection(
panEnabled: Boolean,
Expand Down
20 changes: 15 additions & 5 deletions cropper/src/main/java/com/smarttoolfactory/cropper/ImageCropper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,20 @@ fun ImageCropper(

val cropType = cropProperties.cropType
val contentScale = cropProperties.contentScale

val fixedAspectRatio = cropProperties.fixedAspectRatio
val cropOutline = cropProperties.cropOutlineProperty.cropOutline

// these keys are for resetting cropper when image width/height, contentScale or
// overlay aspect ratio changes
val resetKeys =
getResetKeys(scaledImageBitmap, imageWidthPx, imageHeightPx, contentScale, cropType)
getResetKeys(
scaledImageBitmap,
imageWidthPx,
imageHeightPx,
contentScale,
cropType,
fixedAspectRatio
)

val cropState = rememberCropState(
imageSize = IntSize(bitmapWidth, bitmapHeight),
Expand Down Expand Up @@ -348,19 +355,22 @@ private fun getResetKeys(
imageWidthPx: Int,
imageHeightPx: Int,
contentScale: ContentScale,
cropType: CropType
cropType: CropType,
fixedAspectRatio: Boolean,
) = remember(
scaledImageBitmap,
imageWidthPx,
imageHeightPx,
contentScale,
cropType
cropType,
fixedAspectRatio,
) {
arrayOf(
scaledImageBitmap,
imageWidthPx,
imageHeightPx,
contentScale,
cropType
cropType,
fixedAspectRatio,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ val aspectRatios = listOf(
),
CropAspectRatio(
title = "Original",
shape = createRectShape(AspectRatio.Unspecified),
aspectRatio = AspectRatio.Unspecified
shape = createRectShape(AspectRatio.Original),
aspectRatio = AspectRatio.Original
),
CropAspectRatio(
title = "1:1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ import androidx.compose.ui.graphics.Shape
data class CropAspectRatio(
val title: String,
val shape: Shape,
val aspectRatio: AspectRatio = AspectRatio.Unspecified,
val aspectRatio: AspectRatio = AspectRatio.Original,
val icons: List<Int> = listOf()
)

/**
* Value class for containing aspect ratio
* and [AspectRatio.Unspecified] for comparing
* and [AspectRatio.Original] for comparing
*/
@Immutable
data class AspectRatio(val value: Float) {
companion object {
val Unspecified = AspectRatio(-1f)
val Original = AspectRatio(-1f)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ object CropDefaults {
contentScale: ContentScale = ContentScale.Fit,
cropOutlineProperty: CropOutlineProperty,
aspectRatio: AspectRatio = aspectRatios[2].aspectRatio,
overlayRatio:Float = .9f,
overlayRatio: Float = .9f,
pannable: Boolean = true,
fling: Boolean = false,
zoomable: Boolean = true,
rotatable: Boolean = false,
requiredSize: IntSize? = null,
fixedAspectRatio: Boolean = false,
requiredSize: IntSize? = null
): CropProperties {
return CropProperties(
cropType = cropType,
Expand All @@ -52,7 +53,8 @@ object CropDefaults {
fling = fling,
zoomable = zoomable,
rotatable = rotatable,
requiredSize = requiredSize,
fixedAspectRatio = fixedAspectRatio,
requiredSize = requiredSize
)
}

Expand Down Expand Up @@ -97,7 +99,8 @@ data class CropProperties internal constructor(
val rotatable: Boolean,
val zoomable: Boolean,
val maxZoom: Float,
val requiredSize: IntSize? = null,
val fixedAspectRatio: Boolean = false,
val requiredSize: IntSize? = null
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fun rememberCropState(
val zoomable = cropProperties.zoomable
val pannable = cropProperties.pannable
val rotatable = cropProperties.rotatable

val fixedAspectRatio = cropProperties.fixedAspectRatio

return remember(*keys) {
when (cropType) {
Expand Down Expand Up @@ -72,7 +72,8 @@ fun rememberCropState(
zoomable = zoomable,
pannable = pannable,
rotatable = rotatable,
limitPan = true
limitPan = true,
fixedAspectRatio = fixedAspectRatio,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ abstract class CropState internal constructor(
containerSize.width.toFloat(),
containerSize.height.toFloat(),
drawAreaSize.width.toFloat(),
drawAreaSize.height.toFloat(),
aspectRatio,
overlayRatio
),
Expand Down Expand Up @@ -158,7 +157,6 @@ abstract class CropState internal constructor(
containerSize.width.toFloat(),
containerSize.height.toFloat(),
drawAreaSize.width.toFloat(),
drawAreaSize.height.toFloat(),
aspectRatio,
overlayRatio
)
Expand Down Expand Up @@ -398,16 +396,17 @@ abstract class CropState internal constructor(
containerWidth: Float,
containerHeight: Float,
drawAreaWidth: Float,
drawAreaHeight: Float,
aspectRatio: AspectRatio,
coefficient: Float
): Rect {

if (aspectRatio == AspectRatio.Unspecified) {
if (aspectRatio == AspectRatio.Original) {
val imageAspectRatio = imageSize.width.toFloat() / imageSize.height.toFloat()

// Maximum width and height overlay rectangle can be measured with
val overlayWidthMax = drawAreaWidth.coerceAtMost(containerWidth * coefficient)
val overlayHeightMax = drawAreaHeight.coerceAtMost(containerHeight * coefficient)
val overlayHeightMax =
(overlayWidthMax / imageAspectRatio).coerceAtMost(containerHeight * coefficient)

val offsetX = (containerWidth - overlayWidthMax) / 2f
val offsetY = (containerHeight - overlayHeightMax) / 2f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import kotlinx.coroutines.coroutineScope
* @param pannable when set to true pan is enabled
* @param rotatable when set to true rotation is enabled
* @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
* @param fixedAspectRatio when set to true aspect ratio of overlay is fixed
* empty space on sides or edges of parent
*/
class DynamicCropState internal constructor(
Expand All @@ -43,7 +44,8 @@ class DynamicCropState internal constructor(
zoomable: Boolean,
pannable: Boolean,
rotatable: Boolean,
limitPan: Boolean
limitPan: Boolean,
private val fixedAspectRatio: Boolean,
) : CropState(
imageSize = imageSize,
containerSize = containerSize,
Expand Down Expand Up @@ -86,7 +88,7 @@ class DynamicCropState internal constructor(
override suspend fun updateProperties(cropProperties: CropProperties, forceUpdate: Boolean) {
handleSize = cropProperties.handleSize
minOverlaySize = handleSize * 2

super.updateProperties(cropProperties, forceUpdate)
}

Expand Down Expand Up @@ -137,13 +139,23 @@ class DynamicCropState internal constructor(
minDimension = minOverlaySize,
rectTemp = rectTemp,
overlayRect = overlayRect,
change = change
change = change,
aspectRatio = getAspectRatio(),
fixedAspectRatio = fixedAspectRatio,
)

snapOverlayRectTo(newRect)
}
}

private fun getAspectRatio(): Float {
return if (aspectRatio == AspectRatio.Original) {
imageSize.width / imageSize.height.toFloat()
} else {
aspectRatio.value
}
}

override suspend fun onUp(change: PointerInputChange) = coroutineScope {
if (touchRegion != TouchRegion.None) {

Expand Down Expand Up @@ -253,7 +265,6 @@ class DynamicCropState internal constructor(
containerSize.width.toFloat(),
containerSize.height.toFloat(),
drawAreaSize.width.toFloat(),
drawAreaSize.height.toFloat(),
aspectRatio,
overlayRatio
)
Expand Down Expand Up @@ -351,7 +362,9 @@ class DynamicCropState internal constructor(
minDimension: Float,
rectTemp: Rect,
overlayRect: Rect,
change: PointerInputChange
change: PointerInputChange,
aspectRatio: Float,
fixedAspectRatio: Boolean,
): Rect {

val position = change.position
Expand All @@ -368,7 +381,15 @@ class DynamicCropState internal constructor(
// Set position of top left while moving with top left handle and
// limit position to not intersect other handles
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
val top = if (fixedAspectRatio) {
// If aspect ratio is fixed we need to calculate top position based on
// left position and aspect ratio
val width = rectTemp.right - left
val height = width / aspectRatio
rectTemp.bottom - height
} else {
screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
}
Rect(
left = left,
top = top,
Expand All @@ -382,7 +403,15 @@ class DynamicCropState internal constructor(
// Set position of top left while moving with bottom left handle and
// limit position to not intersect other handles
val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension)
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
val bottom = if (fixedAspectRatio) {
// If aspect ratio is fixed we need to calculate bottom position based on
// left position and aspect ratio
val width = rectTemp.right - left
val height = width / aspectRatio
rectTemp.top + height
} else {
screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
}
Rect(
left = left,
top = rectTemp.top,
Expand All @@ -396,7 +425,15 @@ class DynamicCropState internal constructor(
// Set position of top left while moving with top right handle and
// limit position to not intersect other handles
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
val top = screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
val top = if (fixedAspectRatio) {
// If aspect ratio is fixed we need to calculate top position based on
// right position and aspect ratio
val width = right - rectTemp.left
val height = width / aspectRatio
rectTemp.bottom - height
} else {
screenPositionY.coerceAtMost(rectTemp.bottom - minDimension)
}

Rect(
left = rectTemp.left,
Expand All @@ -412,7 +449,15 @@ class DynamicCropState internal constructor(
// Set position of top left while moving with bottom right handle and
// limit position to not intersect other handles
val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension)
val bottom = screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
val bottom = if (fixedAspectRatio) {
// If aspect ratio is fixed we need to calculate bottom position based on
// right position and aspect ratio
val width = right - rectTemp.left
val height = width / aspectRatio
rectTemp.top + height
} else {
screenPositionY.coerceAtLeast(rectTemp.top + minDimension)
}

Rect(
left = rectTemp.left,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fun createRectShape(aspectRatio: AspectRatio): GenericShape {
val width = size.width
val height = size.height
val shapeSize =
if (aspectRatio == AspectRatio.Unspecified) Size(width, height)
if (aspectRatio == AspectRatio.Original) Size(width, height)
else if (value > 1) Size(width = width, height = width / value)
else Size(width = height * value, height = height)

Expand Down Expand Up @@ -154,7 +154,7 @@ fun calculateSizeAndOffsetFromAspectRatio(

val value = aspectRatio.value

val newSize = if (aspectRatio == AspectRatio.Unspecified) {
val newSize = if (aspectRatio == AspectRatio.Original) {
Size(width * coefficient, height * coefficient)
} else if (value > 1) {
Size(
Expand Down

0 comments on commit 4ef269a

Please sign in to comment.