diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 72d840c4ad..cab00d99c1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -276,7 +276,7 @@ class AttachmentsPreviewPresenter( imageEditorState = AttachmentImageEditorState( localMedia = originalLocalMedia, edits = appliedImageEdits, - forceDrawGuidelines = false, + previewDebug = false, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt index d103e6b913..3c8af52ce8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.attachments.preview.imageeditor +import androidx.annotation.FloatRange import androidx.compose.runtime.Immutable import io.element.android.libraries.mediaviewer.api.local.LocalMedia @@ -18,7 +19,7 @@ data class AttachmentImageEditorState( val localMedia: LocalMedia, val edits: AttachmentImageEdits, // For preview only - val forceDrawGuidelines: Boolean, + val previewDebug: Boolean, ) @Immutable @@ -51,10 +52,10 @@ data class AttachmentImageEdits( @Immutable data class NormalizedCropRect( - val left: Float, - val top: Float, - val right: Float, - val bottom: Float, + @FloatRange(from = 0.0, to = 1.0) val left: Float, + @FloatRange(from = 0.0, to = 1.0) val top: Float, + @FloatRange(from = 0.0, to = 1.0) val right: Float, + @FloatRange(from = 0.0, to = 1.0) val bottom: Float, ) { init { require(left in 0f..1f) @@ -71,7 +72,11 @@ data class NormalizedCropRect( val height: Float get() = bottom - top - fun applyChange(dragTarget: CropDragTarget, deltaX: Float, deltaY: Float): NormalizedCropRect = when (dragTarget) { + fun applyChange( + dragTarget: CropDragTarget, + deltaX: Float, + deltaY: Float, + ): NormalizedCropRect = when (dragTarget) { is CropDragTarget.Move -> translate(deltaX, deltaY) is CropDragTarget.Corner -> dragWithCorner(dragTarget, deltaX, deltaY) is CropDragTarget.Edge -> dragWithEdge(dragTarget, deltaX, deltaY) @@ -88,7 +93,11 @@ data class NormalizedCropRect( ) } - private fun dragWithCorner(dragTarget: CropDragTarget.Corner, deltaX: Float, deltaY: Float) = when (dragTarget) { + private fun dragWithCorner( + dragTarget: CropDragTarget.Corner, + deltaX: Float, + deltaY: Float, + ) = when (dragTarget) { CropDragTarget.Corner.TopLeft -> copy( left = (left + deltaX).coerceIn(0f, right - MIN_CROP_SIZE), top = (top + deltaY).coerceIn(0f, bottom - MIN_CROP_SIZE), @@ -107,7 +116,11 @@ data class NormalizedCropRect( ) } - private fun dragWithEdge(dragTarget: CropDragTarget.Edge, deltaX: Float, deltaY: Float) = when (dragTarget) { + private fun dragWithEdge( + dragTarget: CropDragTarget.Edge, + deltaX: Float, + deltaY: Float, + ) = when (dragTarget) { CropDragTarget.Edge.Top -> copy( top = (top + deltaY).coerceIn(0f, bottom - MIN_CROP_SIZE), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt index 09a4390202..df4bb5257f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt @@ -44,13 +44,37 @@ open class AttachmentImageEditorStateProvider : PreviewParameterProvider(null) } val latestCropRect by rememberUpdatedState(state.edits.cropRect) - val drawGuidelines = dragTarget == CropDragTarget.Move || state.forceDrawGuidelines + val drawGuidelines = dragTarget == CropDragTarget.Move || state.previewDebug Box( modifier = Modifier .fillMaxSize() @@ -290,7 +304,7 @@ private fun BoxScope.CropEditorCanvas( imageOffset = imageRect.topLeft, cropRect = latestCropRect, canvasSize = Size(imageRect.width, imageRect.height), - handleTouchRadius = touchRadius.toPx(), + handleTouchRadius = touchRadiusPx, ) }, onDragCancel = { @@ -317,6 +331,9 @@ private fun BoxScope.CropEditorCanvas( imageSize = DpSize(displayedWidthDp, displayedHeightDp), cropRect = state.edits.cropRect, drawGuidelines = drawGuidelines, + previewDebug = state.previewDebug, + touchRadiusPx = touchRadiusPx, + dragTarget = dragTarget, ) } } @@ -327,6 +344,9 @@ private fun CropOverlay( imageSize: DpSize, cropRect: NormalizedCropRect, drawGuidelines: Boolean, + previewDebug: Boolean, + touchRadiusPx: Float, + dragTarget: CropDragTarget?, ) { val borderColor = ElementTheme.colors.iconPrimary val guideColor = ElementTheme.colors.iconPrimary @@ -459,6 +479,32 @@ private fun CropOverlay( handleLength = handleLength, color = handleColor, ) + + if (previewDebug) { + // Draw disk around touchable area + listOf( + CropDragTarget.Edge.Top, + CropDragTarget.Edge.Right, + CropDragTarget.Edge.Bottom, + CropDragTarget.Edge.Left, + CropDragTarget.Corner.TopLeft, + CropDragTarget.Corner.TopRight, + CropDragTarget.Corner.BottomRight, + CropDragTarget.Corner.BottomLeft, + CropDragTarget.Move, + ).forEach { target -> + val color = when (target) { + is CropDragTarget.Move -> Color.Red + is CropDragTarget.Corner -> Color.Blue + is CropDragTarget.Edge -> Color.Green + }.copy(alpha = if (dragTarget == target) 9f else 0.5f) + drawCircle( + color = color, + radius = touchRadiusPx, + center = computeOffset(target, cropRect, Size(size.width, size.height)), + ) + } + } } } @@ -482,17 +528,20 @@ private fun detectDragTarget( canvasSize: Size, handleTouchRadius: Float, ): CropDragTarget? { - val corners = mapOf( - CropDragTarget.Corner.TopLeft to Offset(cropRect.left * canvasSize.width, cropRect.top * canvasSize.height), - CropDragTarget.Edge.Top to Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, cropRect.top * canvasSize.height), - CropDragTarget.Corner.TopRight to Offset(cropRect.right * canvasSize.width, cropRect.top * canvasSize.height), - CropDragTarget.Edge.Right to Offset(cropRect.right * canvasSize.width, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f), - CropDragTarget.Corner.BottomRight to Offset(cropRect.right * canvasSize.width, cropRect.bottom * canvasSize.height), - CropDragTarget.Edge.Bottom to Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, cropRect.bottom * canvasSize.height), - CropDragTarget.Corner.BottomLeft to Offset(cropRect.left * canvasSize.width, cropRect.bottom * canvasSize.height), - CropDragTarget.Edge.Left to Offset(cropRect.left * canvasSize.width, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f), + // Give priority on Move (extra detection of the center of crop area) + // to ensure that user can move a small crop, then to corners and at last to edges. + val handlesArea = mapOf( + CropDragTarget.Move to computeOffset(CropDragTarget.Move, cropRect, canvasSize), + CropDragTarget.Corner.TopLeft to computeOffset(CropDragTarget.Corner.TopLeft, cropRect, canvasSize), + CropDragTarget.Corner.TopRight to computeOffset(CropDragTarget.Corner.TopRight, cropRect, canvasSize), + CropDragTarget.Corner.BottomRight to computeOffset(CropDragTarget.Corner.BottomRight, cropRect, canvasSize), + CropDragTarget.Corner.BottomLeft to computeOffset(CropDragTarget.Corner.BottomLeft, cropRect, canvasSize), + CropDragTarget.Edge.Top to computeOffset(CropDragTarget.Edge.Top, cropRect, canvasSize), + CropDragTarget.Edge.Right to computeOffset(CropDragTarget.Edge.Right, cropRect, canvasSize), + CropDragTarget.Edge.Bottom to computeOffset(CropDragTarget.Edge.Bottom, cropRect, canvasSize), + CropDragTarget.Edge.Left to computeOffset(CropDragTarget.Edge.Left, cropRect, canvasSize), ) - corners.forEach { (target, corner) -> + handlesArea.forEach { (target, corner) -> if ((corner - touchPoint + imageOffset).getDistance() <= handleTouchRadius) { return target } @@ -508,6 +557,22 @@ private fun detectDragTarget( } } +private fun computeOffset( + target: CropDragTarget, + cropRect: NormalizedCropRect, + canvasSize: Size, +) = when (target) { + CropDragTarget.Move -> Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f) + CropDragTarget.Corner.TopLeft -> Offset(cropRect.left * canvasSize.width, cropRect.top * canvasSize.height) + CropDragTarget.Edge.Top -> Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, cropRect.top * canvasSize.height) + CropDragTarget.Corner.TopRight -> Offset(cropRect.right * canvasSize.width, cropRect.top * canvasSize.height) + CropDragTarget.Edge.Right -> Offset(cropRect.right * canvasSize.width, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f) + CropDragTarget.Corner.BottomRight -> Offset(cropRect.right * canvasSize.width, cropRect.bottom * canvasSize.height) + CropDragTarget.Edge.Bottom -> Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, cropRect.bottom * canvasSize.height) + CropDragTarget.Corner.BottomLeft -> Offset(cropRect.left * canvasSize.width, cropRect.bottom * canvasSize.height) + CropDragTarget.Edge.Left -> Offset(cropRect.left * canvasSize.width, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f) +} + // x and y are the coordinates of the corner private fun DrawScope.drawCornerHandle( x: Float,