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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user