mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Add snackbar host to AppScaffold.
This commit is contained in:
committed by
Cody Henthorne
parent
b3f74d37e1
commit
e2b57b55d6
@@ -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(
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
Reference in New Issue
Block a user