mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Keep the capture mode bar from taking taps meant for the text story send.
This commit is contained in:
+11
-5
@@ -46,6 +46,7 @@ import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -79,12 +80,16 @@ private val MODE_SETTLE_SPEC = spring<Float>(dampingRatio = Spring.DampingRatioN
|
||||
* The highlight is fixed to the center of the screen and the bar slides so the selected mode is the one under it. A mode
|
||||
* can be picked by tapping it or by swiping the bar until it is under the highlight; a swipe only selects once it is
|
||||
* released, so the user can slide back and forth before committing.
|
||||
*
|
||||
* @param endReservation Width at the end of the row the bar stops short of claiming, so taps there reach whatever floats
|
||||
* over it. Layout still uses the full row, so this moves nothing.
|
||||
*/
|
||||
@Composable
|
||||
internal fun MediaCaptureModeBar(
|
||||
availableCaptureModes: List<MediaCaptureMode>,
|
||||
selectedCaptureMode: MediaCaptureMode,
|
||||
onEvent: (MediaCaptureScreenEvents) -> Unit,
|
||||
endReservation: Dp = 0.dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -169,18 +174,19 @@ internal fun MediaCaptureModeBar(
|
||||
val modes = measurables.drop(2).map { it.measure(Constraints(maxWidth = modeWidthLimit, minHeight = modeHeight, maxHeight = modeHeight)) }
|
||||
val measuredModeWidth = modes.maxOf { it.width }
|
||||
val barWidth = measuredModeWidth * modes.size + barPadding * 2
|
||||
val containerWidth = if (constraints.hasBoundedWidth) constraints.maxWidth else barWidth
|
||||
val rowWidth = if (constraints.hasBoundedWidth) constraints.maxWidth else barWidth
|
||||
val containerWidth = (rowWidth - endReservation.roundToPx()).coerceAtLeast(0)
|
||||
|
||||
val bar = measurables[0].measure(Constraints.fixed(barWidth, barHeight))
|
||||
val highlight = measurables[1].measure(Constraints.fixed(measuredModeWidth, modeHeight))
|
||||
|
||||
layout(width = containerWidth, height = barHeight) {
|
||||
// The bar slides by however far the centered mode is from the middle of the container, which is where the
|
||||
// highlight always sits.
|
||||
val slide = containerWidth / 2f - barPadding - (centeredMode.value + 0.5f) * measuredModeWidth
|
||||
// The bar slides by however far the centered mode is from the middle of the row, which is where the highlight
|
||||
// always sits.
|
||||
val slide = rowWidth / 2f - barPadding - (centeredMode.value + 0.5f) * measuredModeWidth
|
||||
|
||||
bar.placeRelative(x = slide.roundToInt(), y = 0)
|
||||
highlight.placeRelative(x = (containerWidth - measuredModeWidth) / 2, y = barPadding)
|
||||
highlight.placeRelative(x = (rowWidth - measuredModeWidth) / 2, y = barPadding)
|
||||
|
||||
modes.forEachIndexed { index, mode ->
|
||||
mode.placeRelative(
|
||||
|
||||
+5
-1
@@ -103,6 +103,9 @@ internal fun MediaCaptureScreen(
|
||||
* centered, and the button floats over its end.
|
||||
*
|
||||
* The row keeps a fixed height so neither one moving in or out shifts the other.
|
||||
*
|
||||
* The text story editor floats its own send button over that same end from underneath, so the bar is held off that
|
||||
* corner while the editor is up.
|
||||
*/
|
||||
@Composable
|
||||
private fun MediaCaptureBottomControls(
|
||||
@@ -134,13 +137,14 @@ private fun MediaCaptureBottomControls(
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = CONTROL_FADE_DURATION_MS)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = CONTROL_FADE_DURATION_MS)),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(bottom = MODE_BAR_BOTTOM_INSET)
|
||||
) {
|
||||
MediaCaptureModeBar(
|
||||
availableCaptureModes = state.availableCaptureModes,
|
||||
selectedCaptureMode = state.selectedCaptureMode,
|
||||
onEvent = onEvent,
|
||||
endReservation = if (state.selectedCaptureScreen is MediaSendRoute.Capture.TextStory) endMargin + NEXT_BUTTON_TOUCH_TARGET else 0.dp,
|
||||
modifier = Modifier.endFadingEdge(
|
||||
fadeWidth = MODE_BAR_FADE_WIDTH * fadeFraction,
|
||||
inset = (endMargin + NEXT_BUTTON_CIRCLE_SIZE) * fadeFraction
|
||||
|
||||
+47
-10
@@ -6,8 +6,13 @@
|
||||
package org.signal.mediasend.screens.capture
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.platform.testTag
|
||||
@@ -21,12 +26,14 @@ import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeLeft
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isCloseTo
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -74,10 +81,10 @@ class MediaCaptureScreenTest {
|
||||
fun `Given a flow that offers every mode, when displayed, then the selected one is centered under the highlight`() {
|
||||
setContent(cameraFirstState())
|
||||
|
||||
val bar = composeTestRule.onNodeWithTag(TestTags.MEDIA_CAPTURE_MODE_BAR).getUnclippedBoundsInRoot()
|
||||
val screen = composeTestRule.onNodeWithTag(TestTags.MEDIA_CAPTURE_SCREEN).getUnclippedBoundsInRoot()
|
||||
val selected = composeTestRule.onNodeWithTag(TestTags.MEDIA_CAPTURE_TEXT_STORY_TOGGLE).getUnclippedBoundsInRoot()
|
||||
|
||||
assertThat(selected.centerX.value).isCloseTo(bar.centerX.value, 1f)
|
||||
assertThat(selected.centerX.value).isCloseTo(screen.centerX.value, 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -192,6 +199,28 @@ class MediaCaptureScreenTest {
|
||||
composeTestRule.onNodeWithTag(TestTags.MEDIA_CAPTURE_PHOTO_TOGGLE).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given the text story route, when the editor's send button is tapped, then the tap reaches the editor`() {
|
||||
var sendClicks = 0
|
||||
|
||||
setContent(cameraFirstState()) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = TEXT_STORY_SEND_END_MARGIN, bottom = TEXT_STORY_SEND_BOTTOM_MARGIN)
|
||||
.size(TEXT_STORY_SEND_SIZE)
|
||||
.clickable { sendClicks++ }
|
||||
.testTag(TEXT_STORY_SEND)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(TEXT_STORY_SEND).performClick()
|
||||
|
||||
assertThat(sendClicks).isEqualTo(1)
|
||||
}
|
||||
|
||||
//region The next button over the bar's end
|
||||
|
||||
@Test
|
||||
@@ -251,19 +280,22 @@ class MediaCaptureScreenTest {
|
||||
mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection
|
||||
)
|
||||
|
||||
private fun setContent(state: MediaCaptureState) {
|
||||
private fun setContent(
|
||||
state: MediaCaptureState,
|
||||
textStoryEditorSlot: @Composable () -> Unit = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag(TEXT_STORY_SLOT)
|
||||
)
|
||||
}
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
SignalTheme {
|
||||
MediaCaptureScreen(
|
||||
state = state,
|
||||
onEvent = { events += it },
|
||||
textStoryEditorSlot = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag(TEXT_STORY_SLOT)
|
||||
)
|
||||
}
|
||||
textStoryEditorSlot = textStoryEditorSlot
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -273,6 +305,11 @@ class MediaCaptureScreenTest {
|
||||
|
||||
private companion object {
|
||||
private const val TEXT_STORY_SLOT = "text_story_slot"
|
||||
private const val TEXT_STORY_SEND = "text_story_send"
|
||||
|
||||
private val TEXT_STORY_SEND_SIZE = 48.dp
|
||||
private val TEXT_STORY_SEND_END_MARGIN = 12.dp
|
||||
private val TEXT_STORY_SEND_BOTTOM_MARGIN = 16.dp
|
||||
|
||||
private val MEDIA = Media(
|
||||
uri = "content://capture".toUri(),
|
||||
|
||||
Reference in New Issue
Block a user