Prevent soft keyboard from covering recipient picker floating action button.

This commit is contained in:
jeffrey-signal
2025-11-20 15:21:52 -05:00
parent df07f4fee4
commit 8e06637b4f
3 changed files with 106 additions and 90 deletions

View File

@@ -16,9 +16,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -26,7 +24,6 @@ import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
@@ -187,6 +184,14 @@ private fun AddMembersScreenUi(
if (uiState.isLookingUpRecipient) { if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog() Dialogs.IndeterminateProgressDialog()
} }
},
floatingActionButton = {
Buttons.MediumTonal(
enabled = uiState.newSelections.isNotEmpty(),
onClick = callbacks::onDoneClicked
) {
Text(text = stringResource(R.string.AddMembersActivity__done))
}
} }
) )
} }
@@ -197,37 +202,22 @@ private fun AddMembersRecipientPicker(
callbacks: UiCallbacks, callbacks: UiCallbacks,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Box(modifier = modifier) { RecipientPicker(
RecipientPicker( searchQuery = uiState.searchQuery,
searchQuery = uiState.searchQuery, displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
displayModes = setOf(RecipientPicker.DisplayMode.PUSH), selectionLimits = uiState.selectionLimits,
selectionLimits = uiState.selectionLimits, preselectedRecipients = uiState.existingMembersMinusSelf,
preselectedRecipients = uiState.existingMembersMinusSelf, pendingRecipientSelections = uiState.pendingRecipientSelections,
pendingRecipientSelections = uiState.pendingRecipientSelections, isRefreshing = false,
isRefreshing = false, listBottomPadding = 64.dp,
listBottomPadding = 64.dp, clipListToPadding = false,
clipListToPadding = false, callbacks = RecipientPickerCallbacks(
callbacks = RecipientPickerCallbacks( listActions = callbacks,
listActions = callbacks, findByUsername = callbacks,
findByUsername = callbacks, findByPhoneNumber = callbacks
findByPhoneNumber = callbacks ),
), modifier = modifier.fillMaxSize()
modifier = modifier.fillMaxSize() )
)
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
) {
Buttons.MediumTonal(
enabled = uiState.newSelections.isNotEmpty(),
onClick = callbacks::onDoneClicked
) {
Text(text = stringResource(R.string.AddMembersActivity__done))
}
}
}
} }
private interface UiCallbacks : private interface UiCallbacks :

View File

@@ -21,9 +21,7 @@ import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SizeTransform import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -34,7 +32,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -185,6 +182,35 @@ private fun CreateGroupScreenUi(
if (uiState.isLookingUpRecipient) { if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog() Dialogs.IndeterminateProgressDialog()
} }
},
floatingActionButton = {
AnimatedContent(
targetState = uiState.newSelections.isNotEmpty(),
transitionSpec = {
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = ExitTransition.None
) using SizeTransform(sizeAnimationSpec = { _, _ -> tween(300) })
}
) { hasSelectedContacts ->
if (hasSelectedContacts) {
FilledTonalIconButton(
onClick = callbacks::onNextClicked,
content = {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_end_24),
contentDescription = stringResource(R.string.CreateGroupActivity__accessibility_next)
)
}
)
} else {
Buttons.MediumTonal(
onClick = callbacks::onNextClicked
) {
Text(text = stringResource(R.string.CreateGroupActivity__skip))
}
}
}
} }
) )
} }
@@ -195,56 +221,23 @@ private fun CreateGroupRecipientPicker(
callbacks: UiCallbacks, callbacks: UiCallbacks,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Box(modifier = modifier) { RecipientPicker(
RecipientPicker( searchQuery = uiState.searchQuery,
searchQuery = uiState.searchQuery, displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
displayModes = setOf(RecipientPicker.DisplayMode.PUSH), selectionLimits = uiState.selectionLimits,
selectionLimits = uiState.selectionLimits, pendingRecipientSelections = uiState.pendingRecipientSelections,
pendingRecipientSelections = uiState.pendingRecipientSelections, isRefreshing = false,
isRefreshing = false, listBottomPadding = 64.dp,
listBottomPadding = 64.dp, clipListToPadding = false,
clipListToPadding = false, callbacks = remember(callbacks) {
callbacks = remember(callbacks) { RecipientPickerCallbacks(
RecipientPickerCallbacks( listActions = callbacks,
listActions = callbacks, findByUsername = callbacks,
findByUsername = callbacks, findByPhoneNumber = callbacks
findByPhoneNumber = callbacks )
) },
}, modifier = modifier.fillMaxSize()
modifier = modifier.fillMaxSize() )
)
AnimatedContent(
targetState = uiState.newSelections.isNotEmpty(),
transitionSpec = {
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = ExitTransition.None
) using SizeTransform(sizeAnimationSpec = { _, _ -> tween(300) })
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
) { hasSelectedContacts ->
if (hasSelectedContacts) {
FilledTonalIconButton(
onClick = callbacks::onNextClicked,
content = {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_end_24),
contentDescription = stringResource(R.string.CreateGroupActivity__accessibility_next)
)
}
)
} else {
Buttons.MediumTonal(
onClick = callbacks::onNextClicked
) {
Text(text = stringResource(R.string.CreateGroupActivity__skip))
}
}
}
}
} }
private interface UiCallbacks : private interface UiCallbacks :

View File

@@ -7,7 +7,13 @@ package org.thoughtcrime.securesms.recipients.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -23,6 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.Scaffolds
@@ -36,7 +43,7 @@ import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
/** /**
* Provides the common adaptive layout structure for recipient picker screens. * Provides the common adaptive layout structure for recipient picker screens.
*/ */
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun RecipientPickerScaffold( fun RecipientPickerScaffold(
title: String, title: String,
@@ -44,7 +51,8 @@ fun RecipientPickerScaffold(
onNavigateUpClick: () -> Unit, onNavigateUpClick: () -> Unit,
topAppBarActions: @Composable () -> Unit, topAppBarActions: @Composable () -> Unit,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
primaryContent: @Composable () -> Unit primaryContent: @Composable () -> Unit,
floatingActionButton: (@Composable () -> Unit)? = null
) { ) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = forceSplitPane) val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = forceSplitPane)
@@ -68,7 +76,10 @@ fun RecipientPickerScaffold(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} else { } else {
primaryContent() Box {
primaryContent()
FloatingActionButtonContainer(floatingActionButton)
}
} }
}, },
@@ -79,6 +90,7 @@ fun RecipientPickerScaffold(
) { ) {
Box(modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)) { Box(modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)) {
primaryContent() primaryContent()
FloatingActionButtonContainer(floatingActionButton)
} }
} }
}, },
@@ -93,6 +105,27 @@ fun RecipientPickerScaffold(
) )
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun BoxScope.FloatingActionButtonContainer(
button: (@Composable () -> Unit)?
) {
if (button != null) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.imePadding()
.padding(
start = 16.dp,
end = 16.dp,
bottom = if (WindowInsets.isImeVisible) 0.dp else 16.dp
)
) {
button()
}
}
}
@AllDevicePreviews @AllDevicePreviews
@Composable @Composable
private fun RecipientPickerScaffoldPreview() { private fun RecipientPickerScaffoldPreview() {