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.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -26,7 +24,6 @@ import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable
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.LocalContext
import androidx.compose.ui.res.pluralStringResource
@@ -187,6 +184,14 @@ private fun AddMembersScreenUi(
if (uiState.isLookingUpRecipient) {
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,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
RecipientPicker(
searchQuery = uiState.searchQuery,
displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
selectionLimits = uiState.selectionLimits,
preselectedRecipients = uiState.existingMembersMinusSelf,
pendingRecipientSelections = uiState.pendingRecipientSelections,
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
callbacks = RecipientPickerCallbacks(
listActions = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
),
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))
}
}
}
RecipientPicker(
searchQuery = uiState.searchQuery,
displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
selectionLimits = uiState.selectionLimits,
preselectedRecipients = uiState.existingMembersMinusSelf,
pendingRecipientSelections = uiState.pendingRecipientSelections,
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
callbacks = RecipientPickerCallbacks(
listActions = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
),
modifier = modifier.fillMaxSize()
)
}
private interface UiCallbacks :

View File

@@ -21,9 +21,7 @@ import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
@@ -34,7 +32,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
@@ -185,6 +182,35 @@ private fun CreateGroupScreenUi(
if (uiState.isLookingUpRecipient) {
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,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
RecipientPicker(
searchQuery = uiState.searchQuery,
displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
selectionLimits = uiState.selectionLimits,
pendingRecipientSelections = uiState.pendingRecipientSelections,
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
callbacks = remember(callbacks) {
RecipientPickerCallbacks(
listActions = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
)
},
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))
}
}
}
}
RecipientPicker(
searchQuery = uiState.searchQuery,
displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
selectionLimits = uiState.selectionLimits,
pendingRecipientSelections = uiState.pendingRecipientSelections,
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
callbacks = remember(callbacks) {
RecipientPickerCallbacks(
listActions = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
)
},
modifier = modifier.fillMaxSize()
)
}
private interface UiCallbacks :

View File

@@ -7,7 +7,13 @@ package org.thoughtcrime.securesms.recipients.ui
import androidx.compose.foundation.background
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.imePadding
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
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.res.stringResource
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.Previews
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.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class, ExperimentalLayoutApi::class)
@Composable
fun RecipientPickerScaffold(
title: String,
@@ -44,7 +51,8 @@ fun RecipientPickerScaffold(
onNavigateUpClick: () -> Unit,
topAppBarActions: @Composable () -> Unit,
snackbarHostState: SnackbarHostState,
primaryContent: @Composable () -> Unit
primaryContent: @Composable () -> Unit,
floatingActionButton: (@Composable () -> Unit)? = null
) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = forceSplitPane)
@@ -68,7 +76,10 @@ fun RecipientPickerScaffold(
modifier = Modifier.fillMaxSize()
)
} else {
primaryContent()
Box {
primaryContent()
FloatingActionButtonContainer(floatingActionButton)
}
}
},
@@ -79,6 +90,7 @@ fun RecipientPickerScaffold(
) {
Box(modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)) {
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
@Composable
private fun RecipientPickerScaffoldPreview() {