feat: Add crop and rotate functionality to ImageEditorScreen with options bar
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
package com.rosetta.messenger.ui.chats.components
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -45,7 +54,9 @@ private const val TAG = "ImageEditorScreen"
|
||||
*/
|
||||
enum class EditorTool {
|
||||
NONE,
|
||||
DRAW
|
||||
DRAW,
|
||||
CROP,
|
||||
ROTATE
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,8 +96,32 @@ fun ImageEditorScreen(
|
||||
var showBrushSizeSlider by remember { mutableStateOf(false) }
|
||||
var isSaving by remember { mutableStateOf(false) }
|
||||
|
||||
// Current image URI (can change after crop)
|
||||
var currentImageUri by remember { mutableStateOf(imageUri) }
|
||||
|
||||
// Rotation state
|
||||
var rotationAngle by remember { mutableStateOf(0f) }
|
||||
var isFlippedHorizontally by remember { mutableStateOf(false) }
|
||||
var isFlippedVertically by remember { mutableStateOf(false) }
|
||||
|
||||
// PhotoEditor reference
|
||||
var photoEditor by remember { mutableStateOf<PhotoEditor?>(null) }
|
||||
var photoEditorView by remember { mutableStateOf<PhotoEditorView?>(null) }
|
||||
|
||||
// UCrop launcher
|
||||
val cropLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.let { data ->
|
||||
UCrop.getOutput(data)?.let { croppedUri ->
|
||||
currentImageUri = croppedUri
|
||||
// Reload image in PhotoEditorView
|
||||
photoEditorView?.source?.setImageURI(croppedUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
onDismiss()
|
||||
@@ -180,9 +215,10 @@ fun ImageEditorScreen(
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PhotoEditorView(ctx).apply {
|
||||
photoEditorView = this
|
||||
// Load image - fullscreen
|
||||
source.setImageURI(imageUri)
|
||||
source.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
source.setImageURI(currentImageUri)
|
||||
source.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
|
||||
// Build PhotoEditor
|
||||
photoEditor = PhotoEditor.Builder(ctx, this)
|
||||
@@ -191,6 +227,12 @@ fun ImageEditorScreen(
|
||||
.build()
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
// Apply rotation and flip transformations
|
||||
view.source.rotation = rotationAngle
|
||||
view.source.scaleX = if (isFlippedHorizontally) -1f else 1f
|
||||
view.source.scaleY = if (isFlippedVertically) -1f else 1f
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
@@ -226,6 +268,20 @@ fun ImageEditorScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Rotate/Flip options bar
|
||||
AnimatedVisibility(
|
||||
visible = currentTool == EditorTool.ROTATE,
|
||||
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
|
||||
) {
|
||||
RotateOptionsBar(
|
||||
onRotateLeft = { rotationAngle = (rotationAngle - 90f) % 360f },
|
||||
onRotateRight = { rotationAngle = (rotationAngle + 90f) % 360f },
|
||||
onFlipHorizontal = { isFlippedHorizontally = !isFlippedHorizontally },
|
||||
onFlipVertical = { isFlippedVertically = !isFlippedVertically }
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom toolbar with tools
|
||||
BottomToolbar(
|
||||
currentTool = currentTool,
|
||||
@@ -243,6 +299,20 @@ fun ImageEditorScreen(
|
||||
showColorPicker = true
|
||||
}
|
||||
}
|
||||
EditorTool.CROP -> {
|
||||
currentTool = EditorTool.NONE
|
||||
showColorPicker = false
|
||||
showBrushSizeSlider = false
|
||||
photoEditor?.setBrushDrawingMode(false)
|
||||
// Launch UCrop
|
||||
launchCrop(context, currentImageUri, cropLauncher)
|
||||
}
|
||||
EditorTool.ROTATE -> {
|
||||
currentTool = if (currentTool == EditorTool.ROTATE) EditorTool.NONE else tool
|
||||
showColorPicker = false
|
||||
showBrushSizeSlider = false
|
||||
photoEditor?.setBrushDrawingMode(false)
|
||||
}
|
||||
else -> {
|
||||
currentTool = EditorTool.NONE
|
||||
showColorPicker = false
|
||||
@@ -257,6 +327,15 @@ fun ImageEditorScreen(
|
||||
},
|
||||
onEraserClick = {
|
||||
photoEditor?.brushEraser()
|
||||
},
|
||||
onCropClick = {
|
||||
launchCrop(context, currentImageUri, cropLauncher)
|
||||
},
|
||||
onRotateClick = {
|
||||
currentTool = if (currentTool == EditorTool.ROTATE) EditorTool.NONE else EditorTool.ROTATE
|
||||
showColorPicker = false
|
||||
showBrushSizeSlider = false
|
||||
photoEditor?.setBrushDrawingMode(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -268,7 +347,9 @@ private fun BottomToolbar(
|
||||
currentTool: EditorTool,
|
||||
onToolSelected: (EditorTool) -> Unit,
|
||||
onBrushSizeClick: () -> Unit,
|
||||
onEraserClick: () -> Unit
|
||||
onEraserClick: () -> Unit,
|
||||
onCropClick: () -> Unit,
|
||||
onRotateClick: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
color = Color(0xFF1C1C1E),
|
||||
@@ -282,6 +363,22 @@ private fun BottomToolbar(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Crop tool
|
||||
ToolButton(
|
||||
icon = TablerIcons.Crop,
|
||||
label = "Crop",
|
||||
isSelected = currentTool == EditorTool.CROP,
|
||||
onClick = onCropClick
|
||||
)
|
||||
|
||||
// Rotate tool
|
||||
ToolButton(
|
||||
icon = TablerIcons.Rotate,
|
||||
label = "Rotate",
|
||||
isSelected = currentTool == EditorTool.ROTATE,
|
||||
onClick = onRotateClick
|
||||
)
|
||||
|
||||
// Draw tool
|
||||
ToolButton(
|
||||
icon = TablerIcons.Pencil,
|
||||
@@ -507,3 +604,95 @@ private suspend fun saveEditedImage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate/Flip options bar
|
||||
*/
|
||||
@Composable
|
||||
private fun RotateOptionsBar(
|
||||
onRotateLeft: () -> Unit,
|
||||
onRotateRight: () -> Unit,
|
||||
onFlipHorizontal: () -> Unit,
|
||||
onFlipVertical: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
color = Color(0xFF2C2C2E),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Rotate left
|
||||
ToolButton(
|
||||
icon = TablerIcons.RotateClockwise2,
|
||||
label = "Left",
|
||||
isSelected = false,
|
||||
onClick = onRotateLeft
|
||||
)
|
||||
|
||||
// Rotate right
|
||||
ToolButton(
|
||||
icon = TablerIcons.Rotate2,
|
||||
label = "Right",
|
||||
isSelected = false,
|
||||
onClick = onRotateRight
|
||||
)
|
||||
|
||||
// Flip horizontal
|
||||
ToolButton(
|
||||
icon = TablerIcons.FlipHorizontal,
|
||||
label = "Flip H",
|
||||
isSelected = false,
|
||||
onClick = onFlipHorizontal
|
||||
)
|
||||
|
||||
// Flip vertical
|
||||
ToolButton(
|
||||
icon = TablerIcons.FlipVertical,
|
||||
label = "Flip V",
|
||||
isSelected = false,
|
||||
onClick = onFlipVertical
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch UCrop activity for image cropping
|
||||
*/
|
||||
private fun launchCrop(
|
||||
context: Context,
|
||||
sourceUri: Uri,
|
||||
launcher: androidx.activity.result.ActivityResultLauncher<Intent>
|
||||
) {
|
||||
try {
|
||||
val destinationFile = File(context.cacheDir, "cropped_${System.currentTimeMillis()}.png")
|
||||
val destinationUri = Uri.fromFile(destinationFile)
|
||||
|
||||
val options = UCrop.Options().apply {
|
||||
setCompressionFormat(Bitmap.CompressFormat.PNG)
|
||||
setCompressionQuality(100)
|
||||
setToolbarColor(android.graphics.Color.parseColor("#1C1C1E"))
|
||||
setStatusBarColor(android.graphics.Color.parseColor("#1C1C1E"))
|
||||
setActiveControlsWidgetColor(android.graphics.Color.parseColor("#007AFF"))
|
||||
setToolbarWidgetColor(android.graphics.Color.WHITE)
|
||||
setRootViewBackgroundColor(android.graphics.Color.BLACK)
|
||||
setFreeStyleCropEnabled(true)
|
||||
setShowCropGrid(true)
|
||||
setShowCropFrame(true)
|
||||
setHideBottomControls(false)
|
||||
}
|
||||
|
||||
val intent = UCrop.of(sourceUri, destinationUri)
|
||||
.withOptions(options)
|
||||
.getIntent(context)
|
||||
|
||||
launcher.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error launching crop", e)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user