Add snackbar host to AppScaffold.

This commit is contained in:
jeffrey-signal
2025-10-16 16:56:23 -04:00
committed by Cody Henthorne
parent b3f74d37e1
commit e2b57b55d6
4 changed files with 146 additions and 168 deletions

View File

@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -417,6 +418,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
AppScaffold(
navigator = wrappedNavigator,
paneExpansionState = paneExpansionState,
contentWindowInsets = WindowInsets(),
bottomNavContent = {
if (isNavigationVisible) {
Column(

View File

@@ -50,7 +50,7 @@ import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
import org.thoughtcrime.securesms.window.AppScaffoldWithTopBar
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
@@ -168,7 +168,7 @@ private fun NewConversationScreenUi(
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = uiState.forceSplitPaneOnCompactLandscape)
AppScaffoldWithTopBar(
AppScaffold(
topBarContent = {
Scaffolds.DefaultTopAppBar(
title = if (!isSplitPane) stringResource(R.string.NewConversationActivity__new_message) else "",

View File

@@ -14,11 +14,15 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
@@ -204,30 +208,48 @@ enum class WindowSizeClass(
}
/**
* Composable who's precise layout will depend on the window size class of the device it is being utilized on.
* This is built to be generic so that we can use it throughout the application to support different device classes.
* A top-level scaffold that automatically adapts its layout based on the device's window size class. It is a generic container designed to handle the
* arrangement of navigation rails, top/bottom bars, and list-detail pane management for both compact and large screens.
*
* @param topBarContent An optional top bar that spans across all panes.
*
* @param primaryContent The main content, which is typically the detail view in a split-pane layout.
* @param secondaryContent The secondary content, which is typically the list view in a split-pane layout.
*
* @param navRailContent The side navigation rail, shown on medium and larger screen sizes.
* @param bottomNavContent The bottom navigation bar, shown on compact screen sizes.
*
* @param paneExpansionState Manages the position and expansion of the panes in a list-detail layout.
* @param paneExpansionDragHandle An optional drag handle used to resize panes in the list-detail layout.
*
* @param animatorFactory Provides animations to control how panes enter and exit the screen during navigation.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AppScaffold(
navigator: AppScaffoldNavigator<Any>,
topBarContent: @Composable () -> Unit = {},
primaryContent: @Composable () -> Unit = {},
secondaryContent: @Composable () -> Unit,
navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {},
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default,
secondaryContent: @Composable () -> Unit
snackbarHost: @Composable () -> Unit = {},
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default
) {
val isForcedCompact = WindowSizeClass.checkForcedCompact()
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
if (isForcedCompact) {
ListAndNavigation(
topBarContent = topBarContent,
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass
windowSizeClass = windowSizeClass,
contentWindowInsets = contentWindowInsets
)
return
@@ -236,114 +258,134 @@ fun AppScaffold(
val minPaneWidth = navigator.scaffoldDirective.defaultPanePreferredWidth
val navigationState = navigator.state
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
val animationState = with(animatorFactory) {
this@NavigableListDetailPaneScaffold.getListAnimationState(navigationState)
}
AnimatedPane(
enterTransition = EnterTransition.None,
exitTransition = ExitTransition.None,
modifier = Modifier
.zIndex(0f)
.then(animationState.parentModifier)
) {
Box(
modifier = Modifier
.then(animationState.toModifier())
.clipToBounds()
.layout { measurable, constraints ->
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
val placeable = measurable.measure(
constraints.copy(
minWidth = minPaneWidth.roundToPx(),
maxWidth = width
)
)
layout(constraints.maxWidth, placeable.height) {
placeable.placeRelative(
x = 0,
y = 0
)
}
}
) {
ListAndNavigation(
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass
)
Scaffold(
containerColor = Color.Transparent,
contentWindowInsets = contentWindowInsets,
topBar = topBarContent,
snackbarHost = snackbarHost
) { paddingValues ->
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
val animationState = with(animatorFactory) {
this@NavigableListDetailPaneScaffold.getListAnimationState(navigationState)
}
}
},
detailPane = {
val animationState = with(animatorFactory) {
this@NavigableListDetailPaneScaffold.getDetailAnimationState(navigationState)
}
AnimatedPane(
enterTransition = EnterTransition.None,
exitTransition = ExitTransition.None,
modifier = Modifier
.zIndex(1f)
.then(animationState.parentModifier)
) {
Box(
AnimatedPane(
enterTransition = EnterTransition.None,
exitTransition = ExitTransition.None,
modifier = Modifier
.then(animationState.toModifier())
.clipToBounds()
.layout { measurable, constraints ->
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
val placeable = measurable.measure(
constraints.copy(
minWidth = minPaneWidth.roundToPx(),
maxWidth = width
)
)
layout(constraints.maxWidth, placeable.height) {
placeable.placeRelative(
x = constraints.maxWidth -
max(constraints.maxWidth, placeable.width),
y = 0
)
}
}
.zIndex(0f)
.then(animationState.parentModifier)
) {
primaryContent()
Box(
modifier = Modifier
.then(animationState.toModifier())
.clipToBounds()
.layout { measurable, constraints ->
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
val placeable = measurable.measure(
constraints.copy(
minWidth = minPaneWidth.roundToPx(),
maxWidth = width
)
)
layout(constraints.maxWidth, placeable.height) {
placeable.placeRelative(
x = 0,
y = 0
)
}
}
) {
ListAndNavigation(
topBarContent = { },
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass,
contentWindowInsets = contentWindowInsets
)
}
}
}
},
paneExpansionDragHandle = paneExpansionDragHandle,
paneExpansionState = paneExpansionState
)
},
detailPane = {
val animationState = with(animatorFactory) {
this@NavigableListDetailPaneScaffold.getDetailAnimationState(navigationState)
}
AnimatedPane(
enterTransition = EnterTransition.None,
exitTransition = ExitTransition.None,
modifier = Modifier
.zIndex(1f)
.then(animationState.parentModifier)
) {
Box(
modifier = Modifier
.then(animationState.toModifier())
.clipToBounds()
.layout { measurable, constraints ->
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
val placeable = measurable.measure(
constraints.copy(
minWidth = minPaneWidth.roundToPx(),
maxWidth = width
)
)
layout(constraints.maxWidth, placeable.height) {
placeable.placeRelative(
x = constraints.maxWidth -
max(constraints.maxWidth, placeable.width),
y = 0
)
}
}
) {
primaryContent()
}
}
},
paneExpansionDragHandle = paneExpansionDragHandle,
paneExpansionState = paneExpansionState,
modifier = Modifier.padding(paddingValues)
)
}
}
@Composable
private fun ListAndNavigation(
topBarContent: @Composable () -> Unit,
listContent: @Composable () -> Unit,
navRailContent: @Composable () -> Unit,
bottomNavContent: @Composable () -> Unit,
windowSizeClass: WindowSizeClass
snackbarHost: @Composable () -> Unit = {},
windowSizeClass: WindowSizeClass,
contentWindowInsets: WindowInsets
) {
Row(
modifier = if (windowSizeClass.isLandscape()) {
Modifier.displayCutoutPadding()
} else Modifier
) {
if (windowSizeClass.navigation == Navigation.RAIL) {
navRailContent()
}
Column {
Box(modifier = Modifier.weight(1f)) {
listContent()
Scaffold(
containerColor = Color.Transparent,
topBar = topBarContent,
contentWindowInsets = contentWindowInsets,
snackbarHost = snackbarHost
) { paddingValues ->
Row(
modifier = Modifier
.padding(paddingValues)
.then(if (windowSizeClass.isLandscape()) Modifier.displayCutoutPadding() else Modifier)
) {
if (windowSizeClass.navigation == Navigation.RAIL) {
navRailContent()
}
if (windowSizeClass.navigation == Navigation.BAR) {
bottomNavContent()
Column {
Box(modifier = Modifier.weight(1f)) {
listContent()
}
if (windowSizeClass.navigation == Navigation.BAR) {
bottomNavContent()
}
}
}
}

View File

@@ -7,87 +7,22 @@ package org.thoughtcrime.securesms.window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Wraps [AppScaffold], adding a top app bar that spans across both the list and detail panes.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AppScaffoldWithTopBar(
navigator: AppScaffoldNavigator<Any> = rememberAppScaffoldNavigator(),
topBarContent: @Composable () -> Unit = {},
primaryContent: @Composable () -> Unit = {},
navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {},
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default,
secondaryContent: @Composable () -> Unit
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(
forceSplitPaneOnCompactLandscape = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPaneOnCompactLandscape
)
if (isSplitPane) {
Column {
topBarContent()
AppScaffold(
navigator = navigator,
primaryContent = primaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
paneExpansionState = paneExpansionState,
paneExpansionDragHandle = paneExpansionDragHandle,
animatorFactory = animatorFactory,
secondaryContent = secondaryContent
)
}
} else {
AppScaffold(
navigator = navigator,
primaryContent = primaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
paneExpansionState = paneExpansionState,
paneExpansionDragHandle = paneExpansionDragHandle,
animatorFactory = animatorFactory,
secondaryContent = {
Scaffold(topBar = topBarContent) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
secondaryContent()
}
}
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@AllDevicePreviews
@@ -95,14 +30,13 @@ fun AppScaffoldWithTopBar(
private fun AppScaffoldWithTopBarPreview() {
Previews.Preview {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = false)
AppScaffoldWithTopBar(
AppScaffold(
navigator = rememberAppScaffoldNavigator(),
topBarContent = {
Scaffolds.DefaultTopAppBar(
title = if (!isSplitPane) stringResource(R.string.NewConversationActivity__new_message) else "",
title = "Hello World!",
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = "",