refactor: replace zoom/pan image cropping with fixed-image draggable frame UI in ImageCropBottomSheet
- Remove transform gestures (zoom/pan) in favor of fixed ContentScale.Fit background image - Implement draggable crop frame with corner handles for resize and center drag for repositioning - Add `FrameHandle` composable with circular white/blue indicators at frame corners - Replace complex scale/offset calculations with direct frame coordinate manipulation - Enforce minimum frame size (48dp) and container
This commit is contained in:
parent
c8dff8df40
commit
76ad44a7ca
@ -4,7 +4,7 @@ import android.graphics.Bitmap
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -13,9 +13,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
@ -31,26 +31,30 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
private val HandleSize = 28.dp
|
||||||
|
private val MinFrameSize = 48.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom sheet permettant de recadrer une image avec zoom/pan.
|
* Bottom sheet permettant de recadrer une image.
|
||||||
* Affiche un cadre blanc centré (type scanner) et un overlay sombre.
|
* La photo reste fixe en arrière-plan. L'utilisateur déplace et redimensionne
|
||||||
|
* le cadre de recadrage (overlay sombre + cadre blanc avec poignées aux coins).
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -61,59 +65,42 @@ fun ImageCropBottomSheet(
|
|||||||
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
var containerSize by remember { mutableStateOf(IntSize.Zero) }
|
var containerSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
var scale by remember { mutableFloatStateOf(1f) }
|
|
||||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
|
||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
|
||||||
|
|
||||||
val bmpW = bitmap.width.toFloat()
|
|
||||||
val bmpH = bitmap.height.toFloat()
|
|
||||||
|
|
||||||
val containerW = containerSize.width.toFloat().coerceAtLeast(1f)
|
val containerW = containerSize.width.toFloat().coerceAtLeast(1f)
|
||||||
val containerH = containerSize.height.toFloat().coerceAtLeast(1f)
|
val containerH = containerSize.height.toFloat().coerceAtLeast(1f)
|
||||||
|
|
||||||
val fitScale = remember(containerW, containerH, bmpW, bmpH) {
|
val bmpW = bitmap.width.toFloat()
|
||||||
min(containerW / bmpW, containerH / bmpH)
|
val bmpH = bitmap.height.toFloat()
|
||||||
}
|
val bmpRatio = bmpW / bmpH
|
||||||
|
val containerRatio = containerW / containerH.coerceAtLeast(1f)
|
||||||
|
|
||||||
val initialOffsetX = (containerW - bmpW * fitScale) / 2f
|
val displayedW: Float
|
||||||
val initialOffsetY = (containerH - bmpH * fitScale) / 2f
|
val displayedH: Float
|
||||||
|
val displayedOffsetX: Float
|
||||||
|
val displayedOffsetY: Float
|
||||||
|
|
||||||
val totalScale = fitScale * scale
|
if (bmpRatio > containerRatio) {
|
||||||
val displayW = bmpW * totalScale
|
displayedW = containerW
|
||||||
val displayH = bmpH * totalScale
|
displayedH = containerW / bmpRatio
|
||||||
|
displayedOffsetX = 0f
|
||||||
val frameSize = min(containerW, containerH) * 0.8f
|
displayedOffsetY = (containerH - displayedH) / 2f
|
||||||
val frameLeft = (containerW - frameSize) / 2f
|
|
||||||
val frameTop = (containerH - frameSize) / 2f
|
|
||||||
val frameRight = frameLeft + frameSize
|
|
||||||
val frameBottom = frameTop + frameSize
|
|
||||||
|
|
||||||
// Contraintes de pan pour garder le cadre rempli
|
|
||||||
val minOffsetX = if (displayW > frameSize) {
|
|
||||||
frameLeft - initialOffsetX - displayW
|
|
||||||
} else {
|
} else {
|
||||||
(containerW - displayW) / 2f - initialOffsetX
|
displayedH = containerH
|
||||||
}
|
displayedW = containerH * bmpRatio
|
||||||
val maxOffsetX = if (displayW > frameSize) {
|
displayedOffsetX = (containerW - displayedW) / 2f
|
||||||
frameRight - initialOffsetX
|
displayedOffsetY = 0f
|
||||||
} else {
|
|
||||||
(containerW - displayW) / 2f - initialOffsetX
|
|
||||||
}
|
|
||||||
val minOffsetY = if (displayH > frameSize) {
|
|
||||||
frameTop - initialOffsetY - displayH
|
|
||||||
} else {
|
|
||||||
(containerH - displayH) / 2f - initialOffsetY
|
|
||||||
}
|
|
||||||
val maxOffsetY = if (displayH > frameSize) {
|
|
||||||
frameBottom - initialOffsetY
|
|
||||||
} else {
|
|
||||||
(containerH - displayH) / 2f - initialOffsetY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val totalOffsetX = initialOffsetX + offsetX
|
val scaleToBmp = displayedW / bmpW
|
||||||
val totalOffsetY = initialOffsetY + offsetY
|
val halfHandlePx = with(density) { (HandleSize / 2).toPx() }
|
||||||
|
val minPx = with(density) { MinFrameSize.toPx() }
|
||||||
|
|
||||||
|
var frameLeft by remember { mutableFloatStateOf(containerW * 0.15f) }
|
||||||
|
var frameTop by remember { mutableFloatStateOf(containerH * 0.15f) }
|
||||||
|
var frameRight by remember { mutableFloatStateOf(containerW * 0.85f) }
|
||||||
|
var frameBottom by remember { mutableFloatStateOf(containerH * 0.85f) }
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@ -123,13 +110,13 @@ fun ImageCropBottomSheet(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(520.dp)
|
.height(560.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Ajuster le cadrage",
|
text = "Ajuster le cadrage",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 12.dp)
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -138,135 +125,123 @@ fun ImageCropBottomSheet(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onSizeChanged { containerSize = it }
|
.onSizeChanged { containerSize = it }
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTransformGestures { _, pan, zoom, _ ->
|
|
||||||
val newScale = (scale * zoom).coerceIn(1f, 5f)
|
|
||||||
val scaleChange = newScale / scale
|
|
||||||
scale = newScale
|
|
||||||
|
|
||||||
// Ajuste l'offset pour zoomer autour du centre du cadre
|
|
||||||
val focusX = containerW / 2f
|
|
||||||
val focusY = containerH / 2f
|
|
||||||
val relFocusX = focusX - totalOffsetX
|
|
||||||
val relFocusY = focusY - totalOffsetY
|
|
||||||
|
|
||||||
offsetX = (offsetX + pan.x - relFocusX * (scaleChange - 1f))
|
|
||||||
.coerceIn(minOffsetX, maxOffsetX)
|
|
||||||
offsetY = (offsetY + pan.y - relFocusY * (scaleChange - 1f))
|
|
||||||
.coerceIn(minOffsetY, maxOffsetY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
// Image zoomable/pannable
|
// Photo fixe en arrière-plan (ContentScale.Fit)
|
||||||
Image(
|
Image(
|
||||||
bitmap = bitmap.asImageBitmap(),
|
bitmap = bitmap.asImageBitmap(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.None,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
.graphicsLayer {
|
|
||||||
this.scaleX = totalScale
|
|
||||||
this.scaleY = totalScale
|
|
||||||
this.translationX = totalOffsetX
|
|
||||||
this.translationY = totalOffsetY
|
|
||||||
transformOrigin = androidx.compose.ui.graphics.TransformOrigin(0f, 0f)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Overlay sombre + cadre blanc
|
// Overlay sombre + cadre blanc
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
val fSize = min(size.width, size.height) * 0.8f
|
val fL = frameLeft
|
||||||
val fLeft = (size.width - fSize) / 2f
|
val fT = frameTop
|
||||||
val fTop = (size.height - fSize) / 2f
|
val fR = frameRight
|
||||||
val fRight = fLeft + fSize
|
val fB = frameBottom
|
||||||
val fBottom = fTop + fSize
|
|
||||||
|
|
||||||
// Top
|
// Zone sombre
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color.Black.copy(alpha = 0.5f),
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
topLeft = Offset(0f, 0f),
|
topLeft = Offset(0f, 0f),
|
||||||
size = Size(size.width, fTop)
|
size = Size(size.width, fT)
|
||||||
)
|
)
|
||||||
// Bottom
|
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color.Black.copy(alpha = 0.5f),
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
topLeft = Offset(0f, fBottom),
|
topLeft = Offset(0f, fB),
|
||||||
size = Size(size.width, size.height - fBottom)
|
size = Size(size.width, size.height - fB)
|
||||||
)
|
)
|
||||||
// Left
|
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color.Black.copy(alpha = 0.5f),
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
topLeft = Offset(0f, fTop),
|
topLeft = Offset(0f, fT),
|
||||||
size = Size(fLeft, fSize)
|
size = Size(fL, fB - fT)
|
||||||
)
|
)
|
||||||
// Right
|
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color.Black.copy(alpha = 0.5f),
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
topLeft = Offset(fRight, fTop),
|
topLeft = Offset(fR, fT),
|
||||||
size = Size(size.width - fRight, fSize)
|
size = Size(size.width - fR, fB - fT)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Coins du cadre (type scanner)
|
// Bordure blanche
|
||||||
val cornerLength = fSize * 0.15f
|
drawRect(
|
||||||
val strokeWidth = 3f
|
color = Color.White,
|
||||||
val cornerColor = Color.White
|
topLeft = Offset(fL, fT),
|
||||||
|
size = Size(fR - fL, fB - fT),
|
||||||
// Top-left
|
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f)
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fLeft, fTop),
|
|
||||||
Offset(fLeft + cornerLength, fTop),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fLeft, fTop),
|
|
||||||
Offset(fLeft, fTop + cornerLength),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
// Top-right
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fRight, fTop),
|
|
||||||
Offset(fRight - cornerLength, fTop),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fRight, fTop),
|
|
||||||
Offset(fRight, fTop + cornerLength),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
// Bottom-left
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fLeft, fBottom),
|
|
||||||
Offset(fLeft + cornerLength, fBottom),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fLeft, fBottom),
|
|
||||||
Offset(fLeft, fBottom - cornerLength),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
// Bottom-right
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fRight, fBottom),
|
|
||||||
Offset(fRight - cornerLength, fBottom),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
|
||||||
drawLine(
|
|
||||||
cornerColor,
|
|
||||||
Offset(fRight, fBottom),
|
|
||||||
Offset(fRight, fBottom - cornerLength),
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zone centrale draggable pour déplacer le cadre entier
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(
|
||||||
|
x = with(density) { frameLeft.toDp() },
|
||||||
|
y = with(density) { frameTop.toDp() }
|
||||||
|
)
|
||||||
|
.size(
|
||||||
|
width = with(density) { (frameRight - frameLeft).toDp() },
|
||||||
|
height = with(density) { (frameBottom - frameTop).toDp() }
|
||||||
|
)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
val dx = dragAmount.x
|
||||||
|
val dy = dragAmount.y
|
||||||
|
val w = frameRight - frameLeft
|
||||||
|
val h = frameBottom - frameTop
|
||||||
|
frameLeft = (frameLeft + dx).coerceIn(0f, containerW - w)
|
||||||
|
frameTop = (frameTop + dy).coerceIn(0f, containerH - h)
|
||||||
|
frameRight = frameLeft + w
|
||||||
|
frameBottom = frameTop + h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.zIndex(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poignée coin haut-gauche
|
||||||
|
FrameHandle(
|
||||||
|
x = frameLeft,
|
||||||
|
y = frameTop,
|
||||||
|
halfHandlePx = halfHandlePx,
|
||||||
|
onDrag = { dx, dy ->
|
||||||
|
frameLeft = min(frameLeft + dx, frameRight - minPx).coerceAtLeast(0f)
|
||||||
|
frameTop = min(frameTop + dy, frameBottom - minPx).coerceAtLeast(0f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poignée coin haut-droit
|
||||||
|
FrameHandle(
|
||||||
|
x = frameRight,
|
||||||
|
y = frameTop,
|
||||||
|
halfHandlePx = halfHandlePx,
|
||||||
|
onDrag = { dx, dy ->
|
||||||
|
frameRight = max(frameRight + dx, frameLeft + minPx).coerceAtMost(containerW)
|
||||||
|
frameTop = min(frameTop + dy, frameBottom - minPx).coerceAtLeast(0f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poignée coin bas-gauche
|
||||||
|
FrameHandle(
|
||||||
|
x = frameLeft,
|
||||||
|
y = frameBottom,
|
||||||
|
halfHandlePx = halfHandlePx,
|
||||||
|
onDrag = { dx, dy ->
|
||||||
|
frameLeft = min(frameLeft + dx, frameRight - minPx).coerceAtLeast(0f)
|
||||||
|
frameBottom = max(frameBottom + dy, frameTop + minPx).coerceAtMost(containerH)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poignée coin bas-droit
|
||||||
|
FrameHandle(
|
||||||
|
x = frameRight,
|
||||||
|
y = frameBottom,
|
||||||
|
halfHandlePx = halfHandlePx,
|
||||||
|
onDrag = { dx, dy ->
|
||||||
|
frameRight = max(frameRight + dx, frameLeft + minPx).coerceAtMost(containerW)
|
||||||
|
frameBottom = max(frameBottom + dy, frameTop + minPx).coerceAtMost(containerH)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@ -286,14 +261,31 @@ fun ImageCropBottomSheet(
|
|||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
text = "OK",
|
text = "OK",
|
||||||
onClick = {
|
onClick = {
|
||||||
val cropped = cropAndSaveBitmap(
|
val cropLeft = ((frameLeft - displayedOffsetX) / scaleToBmp).toInt().coerceIn(0, bitmap.width)
|
||||||
bitmap,
|
val cropTop = ((frameTop - displayedOffsetY) / scaleToBmp).toInt().coerceIn(0, bitmap.height)
|
||||||
containerW, containerH,
|
val cropRight = ((frameRight - displayedOffsetX) / scaleToBmp).toInt().coerceIn(cropLeft + 1, bitmap.width)
|
||||||
frameLeft, frameTop, frameSize,
|
val cropBottom = ((frameBottom - displayedOffsetY) / scaleToBmp).toInt().coerceIn(cropTop + 1, bitmap.height)
|
||||||
totalOffsetX, totalOffsetY, totalScale,
|
|
||||||
context.cacheDir
|
val cropW = cropRight - cropLeft
|
||||||
)
|
val cropH = cropBottom - cropTop
|
||||||
onCropComplete(cropped)
|
|
||||||
|
val cropped = Bitmap.createBitmap(bitmap, cropLeft, cropTop, cropW, cropH)
|
||||||
|
val maxSize = 512
|
||||||
|
val out = if (cropW > maxSize || cropH > maxSize) {
|
||||||
|
val ratio = maxSize.toFloat() / max(cropW, cropH)
|
||||||
|
val newW = (cropW * ratio).toInt()
|
||||||
|
val newH = (cropH * ratio).toInt()
|
||||||
|
Bitmap.createScaledBitmap(cropped, newW, newH, true).also {
|
||||||
|
if (it != cropped) cropped.recycle()
|
||||||
|
}
|
||||||
|
} else cropped
|
||||||
|
|
||||||
|
val file = File(context.cacheDir, "item_${System.currentTimeMillis()}.jpg")
|
||||||
|
file.outputStream().use { fos ->
|
||||||
|
out.compress(Bitmap.CompressFormat.JPEG, 85, fos)
|
||||||
|
}
|
||||||
|
if (out != cropped && out != bitmap) out.recycle()
|
||||||
|
onCropComplete(Uri.fromFile(file).toString())
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
large = true
|
large = true
|
||||||
@ -304,42 +296,47 @@ fun ImageCropBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cropAndSaveBitmap(
|
@Composable
|
||||||
bitmap: Bitmap,
|
private fun FrameHandle(
|
||||||
containerW: Float,
|
x: Float,
|
||||||
containerH: Float,
|
y: Float,
|
||||||
frameLeft: Float,
|
halfHandlePx: Float,
|
||||||
frameTop: Float,
|
onDrag: (dx: Float, dy: Float) -> Unit
|
||||||
frameSize: Float,
|
) {
|
||||||
totalOffsetX: Float,
|
val density = LocalDensity.current
|
||||||
totalOffsetY: Float,
|
Box(
|
||||||
totalScale: Float,
|
modifier = Modifier
|
||||||
cacheDir: File
|
.offset(
|
||||||
): String? {
|
x = with(density) { (x - halfHandlePx).toDp() },
|
||||||
return try {
|
y = with(density) { (y - halfHandlePx).toDp() }
|
||||||
val cropX = ((frameLeft - totalOffsetX) / totalScale).toInt().coerceIn(0, bitmap.width)
|
)
|
||||||
val cropY = ((frameTop - totalOffsetY) / totalScale).toInt().coerceIn(0, bitmap.height)
|
.size(HandleSize)
|
||||||
val cropW = (frameSize / totalScale).toInt().coerceIn(1, bitmap.width - cropX)
|
.pointerInput(Unit) {
|
||||||
val cropH = (frameSize / totalScale).toInt().coerceIn(1, bitmap.height - cropY)
|
detectDragGestures { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
val cropped = Bitmap.createBitmap(bitmap, cropX, cropY, cropW, cropH)
|
onDrag(dragAmount.x, dragAmount.y)
|
||||||
val maxSize = 512
|
}
|
||||||
val scaled = if (cropW > maxSize || cropH > maxSize) {
|
}
|
||||||
val ratio = maxSize.toFloat() / maxOf(cropW, cropH)
|
.zIndex(2f),
|
||||||
val newW = (cropW * ratio).toInt()
|
contentAlignment = Alignment.Center
|
||||||
val newH = (cropH * ratio).toInt()
|
) {
|
||||||
Bitmap.createScaledBitmap(cropped, newW, newH, true).also {
|
androidx.compose.foundation.layout.Box(
|
||||||
if (it != cropped) cropped.recycle()
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.zIndex(2f)
|
||||||
|
) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
drawCircle(
|
||||||
|
color = Color.White,
|
||||||
|
radius = size.minDimension / 2f,
|
||||||
|
center = Offset(size.width / 2f, size.height / 2f)
|
||||||
|
)
|
||||||
|
drawCircle(
|
||||||
|
color = Color(0xFF1976D2),
|
||||||
|
radius = size.minDimension / 2f - 2f,
|
||||||
|
center = Offset(size.width / 2f, size.height / 2f)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else cropped
|
|
||||||
|
|
||||||
val file = File(cacheDir, "item_${System.currentTimeMillis()}.jpg")
|
|
||||||
file.outputStream().use { out ->
|
|
||||||
scaled.compress(Bitmap.CompressFormat.JPEG, 85, out)
|
|
||||||
}
|
}
|
||||||
if (scaled != cropped && scaled != bitmap) scaled.recycle()
|
|
||||||
Uri.fromFile(file).toString()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user