From 87932c5fab8013204e8c95498b60f52d9f3b87af Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 26 Jan 2026 12:23:13 +0500 Subject: [PATCH] feat: Add crop and rotate functionality to ImageEditorScreen with options bar --- .../ui/chats/components/ImageEditorScreen.kt | 197 +++++++++++++++++- 1 file changed, 193 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt index 5693ab7..d549086 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt @@ -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(null) } + var photoEditorView by remember { mutableStateOf(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 +) { + 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) + } +}