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 package com.rosetta.messenger.ui.chats.components
import android.app.Activity
import android.content.Context 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.net.Uri
import android.util.Log import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.compose.BackHandler 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.animation.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -45,7 +54,9 @@ private const val TAG = "ImageEditorScreen"
*/ */
enum class EditorTool { enum class EditorTool {
NONE, NONE,
DRAW DRAW,
CROP,
ROTATE
} }
/** /**
@@ -85,8 +96,32 @@ fun ImageEditorScreen(
var showBrushSizeSlider by remember { mutableStateOf(false) } var showBrushSizeSlider by remember { mutableStateOf(false) }
var isSaving 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 // PhotoEditor reference
var photoEditor by remember { mutableStateOf<PhotoEditor?>(null) } 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 { BackHandler {
onDismiss() onDismiss()
@@ -180,9 +215,10 @@ fun ImageEditorScreen(
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
PhotoEditorView(ctx).apply { PhotoEditorView(ctx).apply {
photoEditorView = this
// Load image - fullscreen // Load image - fullscreen
source.setImageURI(imageUri) source.setImageURI(currentImageUri)
source.scaleType = ImageView.ScaleType.CENTER_CROP source.scaleType = ImageView.ScaleType.FIT_CENTER
// Build PhotoEditor // Build PhotoEditor
photoEditor = PhotoEditor.Builder(ctx, this) photoEditor = PhotoEditor.Builder(ctx, this)
@@ -191,6 +227,12 @@ fun ImageEditorScreen(
.build() .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() 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 // Bottom toolbar with tools
BottomToolbar( BottomToolbar(
currentTool = currentTool, currentTool = currentTool,
@@ -243,6 +299,20 @@ fun ImageEditorScreen(
showColorPicker = true 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 -> { else -> {
currentTool = EditorTool.NONE currentTool = EditorTool.NONE
showColorPicker = false showColorPicker = false
@@ -257,6 +327,15 @@ fun ImageEditorScreen(
}, },
onEraserClick = { onEraserClick = {
photoEditor?.brushEraser() 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, currentTool: EditorTool,
onToolSelected: (EditorTool) -> Unit, onToolSelected: (EditorTool) -> Unit,
onBrushSizeClick: () -> Unit, onBrushSizeClick: () -> Unit,
onEraserClick: () -> Unit onEraserClick: () -> Unit,
onCropClick: () -> Unit,
onRotateClick: () -> Unit
) { ) {
Surface( Surface(
color = Color(0xFF1C1C1E), color = Color(0xFF1C1C1E),
@@ -282,6 +363,22 @@ private fun BottomToolbar(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically 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 // Draw tool
ToolButton( ToolButton(
icon = TablerIcons.Pencil, 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)
}
}