mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Add flip actions to image edition
This commit is contained in:
+2
@@ -17,6 +17,8 @@ sealed interface AttachmentsPreviewEvent {
|
|||||||
data object OpenImageEditor : AttachmentsPreviewEvent
|
data object OpenImageEditor : AttachmentsPreviewEvent
|
||||||
data object CloseImageEditor : AttachmentsPreviewEvent
|
data object CloseImageEditor : AttachmentsPreviewEvent
|
||||||
data object RotateImageToTheLeft : AttachmentsPreviewEvent
|
data object RotateImageToTheLeft : AttachmentsPreviewEvent
|
||||||
|
data object FlipImageHorizontally : AttachmentsPreviewEvent
|
||||||
|
data object FlipImageVertically : AttachmentsPreviewEvent
|
||||||
data object ApplyImageEdits : AttachmentsPreviewEvent
|
data object ApplyImageEdits : AttachmentsPreviewEvent
|
||||||
data object ResetImageEdits : AttachmentsPreviewEvent
|
data object ResetImageEdits : AttachmentsPreviewEvent
|
||||||
data class UpdateImageCropRect(val cropRect: NormalizedCropRect) : AttachmentsPreviewEvent
|
data class UpdateImageCropRect(val cropRect: NormalizedCropRect) : AttachmentsPreviewEvent
|
||||||
|
|||||||
+12
@@ -295,6 +295,18 @@ class AttachmentsPreviewPresenter(
|
|||||||
edits = pendingState.edits.rotateAntiClockwise()
|
edits = pendingState.edits.rotateAntiClockwise()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AttachmentsPreviewEvent.FlipImageHorizontally -> {
|
||||||
|
val pendingState = imageEditorState ?: return
|
||||||
|
imageEditorState = pendingState.copy(
|
||||||
|
edits = pendingState.edits.flipHorizontally()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AttachmentsPreviewEvent.FlipImageVertically -> {
|
||||||
|
val pendingState = imageEditorState ?: return
|
||||||
|
imageEditorState = pendingState.copy(
|
||||||
|
edits = pendingState.edits.flipVertically()
|
||||||
|
)
|
||||||
|
}
|
||||||
AttachmentsPreviewEvent.ResetImageEdits -> {
|
AttachmentsPreviewEvent.ResetImageEdits -> {
|
||||||
imageEditorState = imageEditorState?.copy(
|
imageEditorState = imageEditorState?.copy(
|
||||||
edits = AttachmentImageEdits()
|
edits = AttachmentImageEdits()
|
||||||
|
|||||||
+2
@@ -136,6 +136,8 @@ fun AttachmentsPreviewView(
|
|||||||
state.eventSink(AttachmentsPreviewEvent.UpdateImageCropRect(cropRect))
|
state.eventSink(AttachmentsPreviewEvent.UpdateImageCropRect(cropRect))
|
||||||
},
|
},
|
||||||
onRotateClick = { state.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) },
|
onRotateClick = { state.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) },
|
||||||
|
onFlipHorizontallyClick = { state.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally) },
|
||||||
|
onFlipVerticallyClick = { state.eventSink(AttachmentsPreviewEvent.FlipImageVertically) },
|
||||||
onCancelClick = ::postCloseImageEditor,
|
onCancelClick = ::postCloseImageEditor,
|
||||||
onResetClick = ::postResetImageEditor,
|
onResetClick = ::postResetImageEditor,
|
||||||
onDoneClick = ::postApplyImageEdits,
|
onDoneClick = ::postApplyImageEdits,
|
||||||
|
|||||||
+24
-13
@@ -87,30 +87,30 @@ class DefaultAttachmentImageEditor(
|
|||||||
decodedBitmap.recycle()
|
decodedBitmap.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
val rotatedBitmap = normalizedBitmap.rotateQuarterTurns(edits.rotationQuarterTurns)
|
val transformedBitmap = normalizedBitmap.applyEdits(edits)
|
||||||
if (rotatedBitmap !== normalizedBitmap) {
|
if (transformedBitmap !== normalizedBitmap) {
|
||||||
normalizedBitmap.recycle()
|
normalizedBitmap.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
val cropRect = edits.cropRect.toPixelRect(
|
val cropRect = edits.cropRect.toPixelRect(
|
||||||
imageWidth = rotatedBitmap.width,
|
imageWidth = transformedBitmap.width,
|
||||||
imageHeight = rotatedBitmap.height,
|
imageHeight = transformedBitmap.height,
|
||||||
)
|
)
|
||||||
val isCropUnchanged = cropRect.left == 0 && cropRect.top == 0 &&
|
val isCropUnchanged = cropRect.left == 0 && cropRect.top == 0 &&
|
||||||
cropRect.width() == rotatedBitmap.width && cropRect.height() == rotatedBitmap.height
|
cropRect.width() == transformedBitmap.width && cropRect.height() == transformedBitmap.height
|
||||||
val croppedBitmap = if (isCropUnchanged) {
|
val croppedBitmap = if (isCropUnchanged) {
|
||||||
rotatedBitmap
|
transformedBitmap
|
||||||
} else {
|
} else {
|
||||||
Bitmap.createBitmap(
|
Bitmap.createBitmap(
|
||||||
rotatedBitmap,
|
transformedBitmap,
|
||||||
cropRect.left,
|
cropRect.left,
|
||||||
cropRect.top,
|
cropRect.top,
|
||||||
cropRect.width(),
|
cropRect.width(),
|
||||||
cropRect.height(),
|
cropRect.height(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (croppedBitmap !== rotatedBitmap) {
|
if (croppedBitmap !== transformedBitmap) {
|
||||||
rotatedBitmap.recycle()
|
transformedBitmap.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
val editedMediaDir = File(context.cacheDir, EDITED_MEDIA_DIR_NAME).apply { mkdirs() }
|
val editedMediaDir = File(context.cacheDir, EDITED_MEDIA_DIR_NAME).apply { mkdirs() }
|
||||||
@@ -141,11 +141,22 @@ internal fun exportedMimeTypeFor(sourceMimeType: String?): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Bitmap.rotateQuarterTurns(quarterTurns: Int): Bitmap {
|
private fun Bitmap.applyEdits(edits: AttachmentImageEdits): Bitmap {
|
||||||
val normalizedTurns = (quarterTurns % 4 + 4) % 4
|
val normalizedTurns = (edits.rotationQuarterTurns % 4 + 4) % 4
|
||||||
if (normalizedTurns == 0) return this
|
if (normalizedTurns == 0 && !edits.isFlippedHorizontally && !edits.isFlippedVertically) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
val centerX = width / 2f
|
||||||
|
val centerY = height / 2f
|
||||||
val matrix = Matrix().apply {
|
val matrix = Matrix().apply {
|
||||||
postRotate(normalizedTurns * 90f)
|
val scaleX = if (edits.isFlippedHorizontally) -1f else 1f
|
||||||
|
val scaleY = if (edits.isFlippedVertically) -1f else 1f
|
||||||
|
if (scaleX < 0f || scaleY < 0f) {
|
||||||
|
postScale(scaleX, scaleY, centerX, centerY)
|
||||||
|
}
|
||||||
|
if (normalizedTurns != 0) {
|
||||||
|
postRotate(normalizedTurns * 90f, centerX, centerY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-7
@@ -26,6 +26,8 @@ data class AttachmentImageEditorState(
|
|||||||
data class AttachmentImageEdits(
|
data class AttachmentImageEdits(
|
||||||
val cropRect: NormalizedCropRect = NormalizedCropRect.default(),
|
val cropRect: NormalizedCropRect = NormalizedCropRect.default(),
|
||||||
val rotationQuarterTurns: Int = 0,
|
val rotationQuarterTurns: Int = 0,
|
||||||
|
val isFlippedHorizontally: Boolean = false,
|
||||||
|
val isFlippedVertically: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val normalizedRotationQuarterTurns: Int
|
val normalizedRotationQuarterTurns: Int
|
||||||
get() = rotationQuarterTurns % 4
|
get() = rotationQuarterTurns % 4
|
||||||
@@ -34,18 +36,32 @@ data class AttachmentImageEdits(
|
|||||||
get() = normalizedRotationQuarterTurns * 90
|
get() = normalizedRotationQuarterTurns * 90
|
||||||
|
|
||||||
val hasChanges: Boolean
|
val hasChanges: Boolean
|
||||||
get() = cropRect != NormalizedCropRect.default() || normalizedRotationQuarterTurns != 0
|
get() = cropRect != NormalizedCropRect.default() ||
|
||||||
|
normalizedRotationQuarterTurns != 0 ||
|
||||||
|
isFlippedHorizontally ||
|
||||||
|
isFlippedVertically
|
||||||
|
|
||||||
fun rotateAntiClockwise(): AttachmentImageEdits {
|
fun rotateAntiClockwise(): AttachmentImageEdits {
|
||||||
return copy(
|
return copy(
|
||||||
rotationQuarterTurns = (normalizedRotationQuarterTurns + 3) % 4,
|
rotationQuarterTurns = (normalizedRotationQuarterTurns + 3) % 4,
|
||||||
// Also update the crop rect to keep the same selected area
|
// Also update the crop rect to keep the same selected area
|
||||||
cropRect = NormalizedCropRect(
|
cropRect = cropRect.rotateAntiClockwise()
|
||||||
left = cropRect.top,
|
)
|
||||||
top = 1f - cropRect.right,
|
}
|
||||||
right = cropRect.bottom,
|
|
||||||
bottom = 1f - cropRect.left,
|
fun flipHorizontally(): AttachmentImageEdits {
|
||||||
)
|
return copy(
|
||||||
|
isFlippedHorizontally = !isFlippedHorizontally,
|
||||||
|
// Also update the crop rect to keep the same selected area
|
||||||
|
cropRect = cropRect.flipHorizontally(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flipVertically(): AttachmentImageEdits {
|
||||||
|
return copy(
|
||||||
|
isFlippedVertically = !isFlippedVertically,
|
||||||
|
// Also update the crop rect to keep the same selected area
|
||||||
|
cropRect = cropRect.flipVertically(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +151,23 @@ data class NormalizedCropRect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun rotateAntiClockwise() = copy(
|
||||||
|
left = top,
|
||||||
|
top = 1f - right,
|
||||||
|
right = bottom,
|
||||||
|
bottom = 1f - left,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun flipHorizontally() = copy(
|
||||||
|
left = 1f - right,
|
||||||
|
right = 1f - left,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun flipVertically() = copy(
|
||||||
|
top = 1f - bottom,
|
||||||
|
bottom = 1f - top,
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun default() = NormalizedCropRect(
|
fun default() = NormalizedCropRect(
|
||||||
left = DEFAULT_CROP_MARGIN,
|
left = DEFAULT_CROP_MARGIN,
|
||||||
|
|||||||
+10
@@ -75,6 +75,16 @@ open class AttachmentImageEditorStateProvider : PreviewParameterProvider<Attachm
|
|||||||
),
|
),
|
||||||
previewDebug = true,
|
previewDebug = true,
|
||||||
),
|
),
|
||||||
|
anAttachmentImageEditorState(
|
||||||
|
edits = AttachmentImageEdits(
|
||||||
|
cropRect = caterpillarCrop,
|
||||||
|
).flipHorizontally(),
|
||||||
|
),
|
||||||
|
anAttachmentImageEditorState(
|
||||||
|
edits = AttachmentImageEdits(
|
||||||
|
cropRect = caterpillarCrop,
|
||||||
|
).flipVertically(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
-15
@@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.attachments.preview.imageedito
|
|||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -90,6 +88,8 @@ fun AttachmentImageEditorView(
|
|||||||
state: AttachmentImageEditorState,
|
state: AttachmentImageEditorState,
|
||||||
onCropRectChange: (NormalizedCropRect) -> Unit,
|
onCropRectChange: (NormalizedCropRect) -> Unit,
|
||||||
onRotateClick: () -> Unit,
|
onRotateClick: () -> Unit,
|
||||||
|
onFlipHorizontallyClick: () -> Unit,
|
||||||
|
onFlipVerticallyClick: () -> Unit,
|
||||||
onResetClick: () -> Unit,
|
onResetClick: () -> Unit,
|
||||||
onCancelClick: () -> Unit,
|
onCancelClick: () -> Unit,
|
||||||
onDoneClick: () -> Unit,
|
onDoneClick: () -> Unit,
|
||||||
@@ -101,8 +101,18 @@ fun AttachmentImageEditorView(
|
|||||||
state.edits.rotationDegrees,
|
state.edits.rotationDegrees,
|
||||||
state.edits.rotationDegrees,
|
state.edits.rotationDegrees,
|
||||||
)
|
)
|
||||||
val rotateButtonBackground = ElementTheme.colors.bgCanvasDefault
|
val flipHorizontalLabel = stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally)
|
||||||
|
val flipHorizontalState = if (state.edits.isFlippedHorizontally) {
|
||||||
|
stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally_state_flipped)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally_state_original)
|
||||||
|
}
|
||||||
|
val flipVerticalLabel = stringResource(R.string.screen_image_edition_a11y_flip_image_vertically)
|
||||||
|
val flipVerticalState = if (state.edits.isFlippedVertically) {
|
||||||
|
stringResource(R.string.screen_image_edition_a11y_flip_image_vertically_state_flipped)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_image_edition_a11y_flip_image_vertically_state_original)
|
||||||
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -158,37 +168,57 @@ fun AttachmentImageEditorView(
|
|||||||
onClick = onResetClick,
|
onClick = onResetClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Row(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(2f),
|
||||||
contentAlignment = Alignment.Center,
|
// Center the content horizontally
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onFlipHorizontallyClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
contentDescription = flipHorizontalLabel
|
||||||
|
stateDescription = flipHorizontalState
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.FlipHorizontal(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onRotateClick,
|
onClick = onRotateClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
|
||||||
color = rotateButtonBackground,
|
|
||||||
shape = CircleShape,
|
|
||||||
)
|
|
||||||
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, CircleShape)
|
|
||||||
.clearAndSetSemantics {
|
.clearAndSetSemantics {
|
||||||
contentDescription = rotateContentDescription
|
contentDescription = rotateContentDescription
|
||||||
stateDescription = rotationStateDescription
|
stateDescription = rotationStateDescription
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
|
||||||
.size(22.dp),
|
|
||||||
imageVector = CompoundIcons.RotateLeft(),
|
imageVector = CompoundIcons.RotateLeft(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = onFlipVerticallyClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
contentDescription = flipVerticalLabel
|
||||||
|
stateDescription = flipVerticalState
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.FlipVertical(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
text = stringResource(CommonStrings.action_done),
|
text = stringResource(CommonStrings.action_save),
|
||||||
onClick = onDoneClick,
|
onClick = onDoneClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -204,6 +234,8 @@ private fun BoxScope.CropEditorCanvas(
|
|||||||
) {
|
) {
|
||||||
var imageSize by remember(state.localMedia.uri) { mutableStateOf(IntSize.Zero) }
|
var imageSize by remember(state.localMedia.uri) { mutableStateOf(IntSize.Zero) }
|
||||||
val rotationQuarterTurns = state.edits.normalizedRotationQuarterTurns
|
val rotationQuarterTurns = state.edits.normalizedRotationQuarterTurns
|
||||||
|
val flipScaleX = if (state.edits.isFlippedHorizontally) -1f else 1f
|
||||||
|
val flipScaleY = if (state.edits.isFlippedVertically) -1f else 1f
|
||||||
|
|
||||||
var imageRect by remember { mutableStateOf(Rect.Zero) }
|
var imageRect by remember { mutableStateOf(Rect.Zero) }
|
||||||
|
|
||||||
@@ -257,6 +289,10 @@ private fun BoxScope.CropEditorCanvas(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.requiredSize(imageLayoutWidthDp, imageLayoutHeightDp)
|
.requiredSize(imageLayoutWidthDp, imageLayoutHeightDp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = flipScaleX
|
||||||
|
scaleY = flipScaleY
|
||||||
|
}
|
||||||
.graphicsLayer { rotationZ = rotationQuarterTurns * 90f },
|
.graphicsLayer { rotationZ = rotationQuarterTurns * 90f },
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
@@ -266,6 +302,10 @@ private fun BoxScope.CropEditorCanvas(
|
|||||||
contentDescription = stringResource(CommonStrings.common_image),
|
contentDescription = stringResource(CommonStrings.common_image),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.requiredSize(imageLayoutWidthDp, imageLayoutHeightDp)
|
.requiredSize(imageLayoutWidthDp, imageLayoutHeightDp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = flipScaleX
|
||||||
|
scaleY = flipScaleY
|
||||||
|
}
|
||||||
.graphicsLayer { rotationZ = rotationQuarterTurns * 90f },
|
.graphicsLayer { rotationZ = rotationQuarterTurns * 90f },
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
onState = { painterState ->
|
onState = { painterState ->
|
||||||
@@ -642,6 +682,8 @@ internal fun AttachmentImageEditorViewPreview(
|
|||||||
state = state,
|
state = state,
|
||||||
onCropRectChange = {},
|
onCropRectChange = {},
|
||||||
onRotateClick = {},
|
onRotateClick = {},
|
||||||
|
onFlipHorizontallyClick = {},
|
||||||
|
onFlipVerticallyClick = {},
|
||||||
onResetClick = {},
|
onResetClick = {},
|
||||||
onCancelClick = {},
|
onCancelClick = {},
|
||||||
onDoneClick = {},
|
onDoneClick = {},
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
<string name="emoji_picker_category_places">"Travel & Places"</string>
|
<string name="emoji_picker_category_places">"Travel & Places"</string>
|
||||||
<string name="emoji_picker_category_recent">"Recent emojis"</string>
|
<string name="emoji_picker_category_recent">"Recent emojis"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Symbols"</string>
|
<string name="emoji_picker_category_symbols">"Symbols"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"Flip image horizontally"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Flipped horizontally"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"Flip image vertically"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Flipped vertically"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Original"</string>
|
||||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Rotate the image to the left"</string>
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"Rotate the image to the left"</string>
|
||||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
<item quantity="one">"%1$d degree"</item>
|
<item quantity="one">"%1$d degree"</item>
|
||||||
|
|||||||
+33
-2
@@ -624,7 +624,11 @@ class AttachmentsPreviewPresenterTest {
|
|||||||
val croppedState = awaitItem()
|
val croppedState = awaitItem()
|
||||||
croppedState.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft)
|
croppedState.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft)
|
||||||
val rotatedState = awaitItem()
|
val rotatedState = awaitItem()
|
||||||
rotatedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits)
|
rotatedState.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally)
|
||||||
|
val flippedHorizontallyState = awaitItem()
|
||||||
|
flippedHorizontallyState.eventSink(AttachmentsPreviewEvent.FlipImageVertically)
|
||||||
|
val flippedState = awaitItem()
|
||||||
|
flippedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits)
|
||||||
|
|
||||||
val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last()
|
val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last()
|
||||||
assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri)
|
assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri)
|
||||||
@@ -638,9 +642,36 @@ class AttachmentsPreviewPresenterTest {
|
|||||||
right = cropRect.bottom,
|
right = cropRect.bottom,
|
||||||
bottom = 1f - cropRect.left,
|
bottom = 1f - cropRect.left,
|
||||||
)
|
)
|
||||||
reopenedState.imageEditorState.edits.cropRect.assertIsSimilarTo(rotatedCropRect)
|
val flippedCropRect = NormalizedCropRect(
|
||||||
|
left = 1f - rotatedCropRect.right,
|
||||||
|
top = 1f - rotatedCropRect.bottom,
|
||||||
|
right = 1f - rotatedCropRect.left,
|
||||||
|
bottom = 1f - rotatedCropRect.top,
|
||||||
|
)
|
||||||
|
reopenedState.imageEditorState.edits.cropRect.assertIsSimilarTo(flippedCropRect)
|
||||||
assertThat(reopenedState.imageEditorState.edits.rotationQuarterTurns).isEqualTo(3)
|
assertThat(reopenedState.imageEditorState.edits.rotationQuarterTurns).isEqualTo(3)
|
||||||
assertThat(reopenedState.imageEditorState.edits.rotationDegrees).isEqualTo(270)
|
assertThat(reopenedState.imageEditorState.edits.rotationDegrees).isEqualTo(270)
|
||||||
|
assertThat(reopenedState.imageEditorState.edits.isFlippedHorizontally).isTrue()
|
||||||
|
assertThat(reopenedState.imageEditorState.edits.isFlippedVertically).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - image editor flip events update edits`() = runTest {
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(displayMediaQualitySelectorViews = true)
|
||||||
|
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor)
|
||||||
|
val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last()
|
||||||
|
|
||||||
|
editorState.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally)
|
||||||
|
val flippedHorizontallyState = awaitItem()
|
||||||
|
assertThat(flippedHorizontallyState.imageEditorState?.edits?.isFlippedHorizontally).isTrue()
|
||||||
|
|
||||||
|
flippedHorizontallyState.eventSink(AttachmentsPreviewEvent.FlipImageVertically)
|
||||||
|
val flippedState = awaitItem()
|
||||||
|
assertThat(flippedState.imageEditorState?.edits?.isFlippedVertically).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
@@ -42,4 +42,37 @@ class AttachmentImageEditsTest {
|
|||||||
assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.8f)
|
assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.8f)
|
||||||
assertThat(result.hasChanges).isTrue()
|
assertThat(result.hasChanges).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `flip horizontally updates crop and change tracking`() {
|
||||||
|
val sut = AttachmentImageEdits(
|
||||||
|
cropRect = NormalizedCropRect(
|
||||||
|
left = 0.1f,
|
||||||
|
top = 0.3f,
|
||||||
|
right = 0.6f,
|
||||||
|
bottom = 0.9f,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val result = sut.flipHorizontally()
|
||||||
|
assertThat(result.isFlippedHorizontally).isTrue()
|
||||||
|
assertThat(result.cropRect.left).isWithin(0.0001f).of(0.4f)
|
||||||
|
assertThat(result.cropRect.right).isWithin(0.0001f).of(0.9f)
|
||||||
|
assertThat(result.cropRect.top).isWithin(0.0001f).of(0.3f)
|
||||||
|
assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.9f)
|
||||||
|
assertThat(result.hasChanges).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `flip vertical twice resets to default state`() {
|
||||||
|
val edits = AttachmentImageEdits().flipVertically().flipVertically()
|
||||||
|
assertThat(edits.isFlippedVertically).isFalse()
|
||||||
|
assertThat(edits.hasChanges).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `flip horizontally twice resets to default state`() {
|
||||||
|
val edits = AttachmentImageEdits().flipHorizontally().flipHorizontally()
|
||||||
|
assertThat(edits.isFlippedVertically).isFalse()
|
||||||
|
assertThat(edits.hasChanges).isFalse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user