Fix button width jitter using rememberTextMeasurer to derive stable min-width
This commit is contained in:
+33
-16
@@ -10,10 +10,9 @@ package io.element.android.features.preferences.impl.userstatus
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.InputTransformation
|
||||
@@ -29,8 +28,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
@@ -38,8 +39,6 @@ import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
@@ -48,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.ModalBottomShe
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -128,7 +128,9 @@ private fun CurrentStatusRow(
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(text = text) },
|
||||
leadingContent = ListItemContent.Text(emoji),
|
||||
leadingContent = ListItemContent.Custom({
|
||||
Text(text = emoji, modifier = Modifier.size(24.dp))
|
||||
}),
|
||||
trailingContent = ListItemContent.Custom({
|
||||
IconButton(onClick = onClear) {
|
||||
Icon(imageVector = CompoundIcons.Close(), contentDescription = null)
|
||||
@@ -178,32 +180,47 @@ private fun CustomStatusInputRow(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } }
|
||||
val saveLabel = stringResource(CommonStrings.action_save)
|
||||
val cancelLabel = stringResource(CommonStrings.action_cancel)
|
||||
val textStyle = ElementTheme.typography.fontBodyLgMedium
|
||||
val density = LocalDensity.current
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
// Measure both labels and derive a stable min-width so the button never resizes.
|
||||
// 24.dp = 12.dp start + 12.dp end padding of TextButton (Large, no icon).
|
||||
val minButtonWidth = remember(textMeasurer, textStyle, saveLabel, cancelLabel) {
|
||||
val saveWidth = textMeasurer.measure(saveLabel, textStyle).size.width
|
||||
val cancelWidth = textMeasurer.measure(cancelLabel, textStyle).size.width
|
||||
with(density) { maxOf(saveWidth, cancelWidth).toDp() } + 24.dp
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = textFieldState,
|
||||
placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint),
|
||||
inputTransformation = InputTransformation.maxLength(maxLength = 30),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
onKeyboardAction = { if (hasText) onConfirm() },
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
trailingIcon = if (hasText) {
|
||||
{
|
||||
IconButton(onClick = { textFieldState.clearText() }) {
|
||||
Icon(imageVector = CompoundIcons.Close(), contentDescription = null)
|
||||
trailingIcon = {
|
||||
if (hasText) {
|
||||
Box(modifier = Modifier.clickable {
|
||||
textFieldState.clearText()
|
||||
}) {
|
||||
Icon(imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_cancel))
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
}
|
||||
)
|
||||
},
|
||||
trailingContent = ListItemContent.Custom({
|
||||
if (hasText) {
|
||||
TextButton(onClick = onConfirm, text = stringResource(CommonStrings.action_save))
|
||||
} else {
|
||||
TextButton(onClick = onCancel, text = stringResource(CommonStrings.action_cancel))
|
||||
}
|
||||
TextButton(
|
||||
onClick = if (hasText) onConfirm else onCancel,
|
||||
text = if (hasText) saveLabel else cancelLabel,
|
||||
modifier = Modifier.widthIn(min = minButtonWidth),
|
||||
)
|
||||
}),
|
||||
leadingContent = ListItemContent.Custom( {
|
||||
leadingContent = ListItemContent.Custom({
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
border = BorderStroke(1.dp, ElementTheme.colors.bgSubtleSecondary),
|
||||
|
||||
Reference in New Issue
Block a user