mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 20:01:55 +00:00
Merge pull request #6949 from element-hq/feature/bma/flipImage
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user