feat: Add crop and rotate functionality to ImageEditorScreen with options bar

This commit is contained in:
k1ngsterr1
2026-01-26 12:23:13 +05:00
parent 522746d3da
commit 87932c5fab

View File

@@ -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)
}
}