From cf4bee8a42efbb2c439e13d3e7d4c1e02d2592d6 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 2 Sep 2026 16:06:42 -0300 Subject: [PATCH] AppScaffold removal. --- .../chats/ConversationLoadingMask.kt | 49 +- .../components/InsetAwareConstraintLayout.kt | 17 +- .../recipients/ui/RecipientPickerScaffold.kt | 105 ++-- .../securesms/window/AppScaffold.kt | 457 ------------------ .../window/AppScaffoldAnimationState.kt | 122 ----- .../securesms/window/AppScaffoldAnimators.kt | 253 ---------- .../securesms/window/AppScaffoldNavigator.kt | 141 ------ .../securesms/window/AppScaffoldWithTopBar.kt | 75 --- .../core/ui/navigation/TransitionSpecs.kt | 18 +- 9 files changed, 136 insertions(+), 1101 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldWithTopBar.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/chats/ConversationLoadingMask.kt b/app/src/main/java/org/thoughtcrime/securesms/chats/ConversationLoadingMask.kt index f2936f3551..19418060cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/chats/ConversationLoadingMask.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/chats/ConversationLoadingMask.kt @@ -14,18 +14,22 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull -import org.thoughtcrime.securesms.window.AppScaffoldAnimationDefaults -import org.thoughtcrime.securesms.window.AppScaffoldAnimationState +import org.signal.core.ui.navigation.TransitionSpecs import kotlin.time.Duration.Companion.seconds /** @@ -74,7 +78,7 @@ fun ConversationLoadingMask( } val chatModifier = Modifier.graphicsLayer { - with(chatAnimationState) { applyChildValues() } + with(chatAnimationState) { applyTo() } } Box(modifier = Modifier.fillMaxSize()) { @@ -86,7 +90,7 @@ fun ConversationLoadingMask( contentDescription = null, modifier = Modifier .graphicsLayer { - with(fakeChatListAnimationState) { applyChildValues() } + with(fakeChatListAnimationState) { applyTo() } } .fillMaxSize() ) @@ -95,11 +99,12 @@ fun ConversationLoadingMask( } @Composable -private fun Transition.fakeChatListAnimationState(): AppScaffoldAnimationState { - val alpha = animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0f else 1f } - val offset = animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) (-48).dp else 0.dp } +private fun Transition.fakeChatListAnimationState(): MaskAnimationState { + val shift = paneShiftOffset + val alpha = animateFloat(transitionSpec = { TransitionSpecs.paneShiftSpec() }) { if (it) 0f else 1f } + val offset = animateDp(transitionSpec = { TransitionSpecs.paneShiftSpec() }) { if (it) -shift else 0.dp } return remember { - AppScaffoldAnimationState( + MaskAnimationState( offset = offset, alpha = alpha ) @@ -107,22 +112,40 @@ private fun Transition.fakeChatListAnimationState(): AppScaffoldAnimati } @Composable -private fun Transition.chatAnimationState(hasFake: Boolean): AppScaffoldAnimationState { - val alpha = animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 1f else 0f } +private fun Transition.chatAnimationState(hasFake: Boolean): MaskAnimationState { + val shift = paneShiftOffset + val alpha = animateFloat(transitionSpec = { TransitionSpecs.paneShiftSpec() }) { if (it) 1f else 0f } return if (!hasFake) { remember { - AppScaffoldAnimationState( + MaskAnimationState( offset = mutableStateOf(0.dp), alpha = alpha ) } } else { - val offset = animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0.dp else 48.dp } + val offset = animateDp(transitionSpec = { TransitionSpecs.paneShiftSpec() }) { if (it) 0.dp else shift } remember { - AppScaffoldAnimationState( + MaskAnimationState( offset = offset, alpha = alpha ) } } } + +private val paneShiftOffset: Dp + @Composable get() = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -TransitionSpecs.PANE_SHIFT_OFFSET else TransitionSpecs.PANE_SHIFT_OFFSET + +/** + * The alpha and horizontal offset a layer is drawn with. + */ +private class MaskAnimationState( + private val offset: State, + private val alpha: State +) { + fun GraphicsLayerScope.applyTo() { + this.alpha = this@MaskAnimationState.alpha.value + this.translationX = offset.value.toPx() + this.clip = true + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index ada85df505..f07b4d4d90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -148,6 +148,20 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( windowInsetsListeners.remove(listener) } + /** + * A guideline's begin and end measure from the left and right edges in either direction, while the views constrained + * to these two use start and end. In RTL that inverts the span, collapsing every view sized by it. + */ + private fun applyHorizontalGuidelines(isLtr: Boolean, parentStart: Int, parentEnd: Int) { + if (isLtr) { + parentStartGuideline?.setGuidelineBegin(parentStart) + parentEndGuideline?.setGuidelineEnd(parentEnd) + } else { + parentStartGuideline?.setGuidelineEnd(parentStart) + parentEndGuideline?.setGuidelineBegin(parentEnd) + } + } + private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) { val isLtr = ViewUtil.isLtr(this) @@ -166,8 +180,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( if (!statusBarShrinking) { statusBarGuideline?.setGuidelineBegin(statusBar) navigationBarGuideline?.setGuidelineEnd(navigationBar) - parentStartGuideline?.setGuidelineBegin(parentStart) - parentEndGuideline?.setGuidelineEnd(parentEnd) + applyHorizontalGuidelines(isLtr, parentStart, parentEnd) windowInsetsListeners.forEach { it.onApplyWindowInsets(statusBar, navigationBar, parentStart, parentEnd) diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPickerScaffold.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPickerScaffold.kt index cda7ff1fa9..0d875d11a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPickerScaffold.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPickerScaffold.kt @@ -8,19 +8,25 @@ 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.BoxWithConstraints import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight 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.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,22 +35,22 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass import org.signal.core.ui.compose.BreakpointPreviews import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons import org.signal.core.ui.detailPaneMaxContentWidth -import org.signal.core.ui.isSplitPane +import org.signal.core.ui.horizontalPartitionDefaultSpacerSize +import org.signal.core.ui.listPaneDefaultPreferredWidth import org.signal.core.ui.rememberIsSplitPane import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.compose.ScreenTitlePane -import org.thoughtcrime.securesms.window.AppScaffold -import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator /** * Provides the common adaptive layout structure for recipient picker screens. */ -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RecipientPickerScaffold( title: String, @@ -57,8 +63,10 @@ fun RecipientPickerScaffold( val isSplitPane = LocalResources.current.rememberIsSplitPane() val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - AppScaffold( - topBarContent = { + Scaffold( + containerColor = Color.Transparent, + contentWindowInsets = WindowInsets.systemBars, + topBar = { Scaffolds.DefaultTopAppBar( title = if (!isSplitPane) title else "", titleContent = { _, titleText -> Text(text = titleText, style = MaterialTheme.typography.titleLarge) }, @@ -68,41 +76,70 @@ fun RecipientPickerScaffold( actions = { topAppBarActions() } ) }, - - secondaryContent = { - if (isSplitPane) { - ScreenTitlePane( - title = title, - modifier = Modifier.fillMaxSize() - ) - } else { - Box { - primaryContent() - FloatingActionButtonContainer(floatingActionButton) - } - } - }, - - primaryContent = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() + snackbarHost = { + SnackbarHost(snackbarHostState) + } + ) { paddingValues -> + if (isSplitPane) { + SplitPaneLayout( + title = title, + windowSizeClass = windowSizeClass, + modifier = Modifier.padding(paddingValues) ) { Box(modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)) { primaryContent() FloatingActionButtonContainer(floatingActionButton) } } - }, + } else { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + primaryContent() + FloatingActionButtonContainer(floatingActionButton) + } + } + } +} - snackbarHost = { - SnackbarHost(snackbarHostState) - }, +/** + * Places the screen title beside [content], capping the title pane at [listPaneDefaultPreferredWidth] and splitting + * the available width evenly below twice that. + */ +@Composable +private fun SplitPaneLayout( + title: String, + windowSizeClass: WindowSizeClass, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val spacerWidth = windowSizeClass.horizontalPartitionDefaultSpacerSize - navigator = rememberAppScaffoldNavigator( - isSplitPane = isSplitPane - ) - ) + BoxWithConstraints(modifier = modifier.fillMaxSize()) { + val titlePaneWidth = ((maxWidth - spacerWidth) / 2).coerceAtMost(windowSizeClass.listPaneDefaultPreferredWidth) + + Row(modifier = Modifier.fillMaxSize()) { + ScreenTitlePane( + title = title, + modifier = Modifier + .width(titlePaneWidth) + .fillMaxHeight() + ) + + Spacer(modifier = Modifier.width(spacerWidth)) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) { + content() + } + } + } } @OptIn(ExperimentalLayoutApi::class) diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt deleted file mode 100644 index c773f457c0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.window - -import android.os.Build -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.SizeTransform -import androidx.compose.animation.core.snap -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -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.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.layout.PaneExpansionState -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope -import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics -import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState -import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.LocalResources -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import kotlinx.coroutines.launch -import org.signal.core.ui.NavigationType -import org.signal.core.ui.compose.BreakpointPreviews -import org.signal.core.ui.compose.Previews -import org.signal.core.ui.rememberIsSplitPane -import org.thoughtcrime.securesms.main.MainFloatingActionButtonsCallback -import org.thoughtcrime.securesms.main.MainNavigationBar -import org.thoughtcrime.securesms.main.MainNavigationBarState -import org.thoughtcrime.securesms.main.MainNavigationRail -import kotlin.math.max - -/** - * 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. - * - * On phone-class layouts (single horizontal partition) running on devices that predate predictive back (API < 33), - * this dispatches to [SinglePaneAppScaffold], which skips [NavigableListDetailPaneScaffold] / [ThreePaneScaffold] and - * its lookahead measurement pass. The scaffold's seek-driven predictive back animation never fires on those devices, - * so we pay no UX cost for the simpler implementation. - * - * @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. Ignored by [SinglePaneAppScaffold]. - * @param paneExpansionDragHandle An optional drag handle used to resize panes in the list-detail layout. Ignored by [SinglePaneAppScaffold]. - * - * @param animatorFactory Provides animations to control how panes enter and exit the screen during navigation. Ignored by [SinglePaneAppScaffold]. - */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun AppScaffold( - navigator: AppScaffoldNavigator, - modifier: Modifier = Modifier, - topBarContent: @Composable () -> Unit = {}, - primaryContent: @Composable () -> Unit = {}, - secondaryContent: @Composable () -> Unit, - navRailContent: @Composable () -> Unit = {}, - bottomNavContent: @Composable () -> Unit = {}, - paneExpansionState: PaneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null, - snackbarHost: @Composable () -> Unit = {}, - contentWindowInsets: WindowInsets = WindowInsets.systemBars, - animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default -) { - val useSimpleScaffold = navigator.scaffoldDirective.maxHorizontalPartitions == 1 && Build.VERSION.SDK_INT < 33 - if (useSimpleScaffold && LocalLayoutDirection.current != LayoutDirection.Rtl) { - SinglePaneAppScaffold( - navigator = navigator, - modifier = modifier, - topBarContent = topBarContent, - primaryContent = primaryContent, - secondaryContent = secondaryContent, - bottomNavContent = bottomNavContent, - snackbarHost = snackbarHost, - contentWindowInsets = contentWindowInsets, - animatorFactory = animatorFactory - ) - } else { - AdaptiveAppScaffold( - navigator = navigator, - modifier = modifier, - topBarContent = topBarContent, - primaryContent = primaryContent, - secondaryContent = secondaryContent, - navRailContent = navRailContent, - bottomNavContent = bottomNavContent, - paneExpansionState = paneExpansionState, - paneExpansionDragHandle = paneExpansionDragHandle, - snackbarHost = snackbarHost, - contentWindowInsets = contentWindowInsets, - animatorFactory = animatorFactory - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -private fun AdaptiveAppScaffold( - navigator: AppScaffoldNavigator, - modifier: Modifier = Modifier, - topBarContent: @Composable () -> Unit = {}, - primaryContent: @Composable () -> Unit = {}, - secondaryContent: @Composable () -> Unit, - navRailContent: @Composable () -> Unit = {}, - bottomNavContent: @Composable () -> Unit = {}, - paneExpansionState: PaneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null, - snackbarHost: @Composable () -> Unit = {}, - contentWindowInsets: WindowInsets = WindowInsets.systemBars, - animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default -) { - val minPaneWidth = navigator.scaffoldDirective.defaultPanePreferredWidth - val navigationState = navigator.state - - Scaffold( - containerColor = Color.Transparent, - contentWindowInsets = contentWindowInsets, - topBar = topBarContent, - snackbarHost = snackbarHost, - modifier = modifier - ) { paddingValues -> - NavigableListDetailPaneScaffold( - navigator = navigator, - listPane = { - val animationState = with(animatorFactory) { - this@NavigableListDetailPaneScaffold.getListAnimationState(navigationState) - } - - AnimatedPane( - enterTransition = EnterTransition.None, - exitTransition = ExitTransition.None, - modifier = Modifier - .zIndex(0f) - .drawWithContent { - with(animationState) { - applyParentValues() - } - } - ) { - Box( - modifier = Modifier - .graphicsLayer { - with(animationState) { - applyChildValues() - } - } - .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, - contentWindowInsets = WindowInsets() // parent scaffold already applies the necessary insets - ) - } - } - }, - detailPane = { - val animationState = with(animatorFactory) { - this@NavigableListDetailPaneScaffold.getDetailAnimationState(navigationState) - } - - AnimatedPane( - enterTransition = EnterTransition.None, - exitTransition = ExitTransition.None, - modifier = Modifier - .zIndex(1f) - .drawWithContent { - with(animationState) { - applyParentValues() - } - } - ) { - Box( - modifier = Modifier - .graphicsLayer { - with(animationState) { - applyChildValues() - } - } - .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 - ) - } - } - ) { - primaryContent() - } - } - }, - paneExpansionDragHandle = paneExpansionDragHandle, - paneExpansionState = paneExpansionState, - modifier = Modifier.padding(paddingValues) - ) - } -} - -/** - * Phone-only scaffold that swaps content between [secondaryContent] and [primaryContent] without using - * [NavigableListDetailPaneScaffold]. Avoids the lookahead measurement pass and deep adaptive layout tree - * that drives ANR on low-end devices. - * - */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -private fun SinglePaneAppScaffold( - navigator: AppScaffoldNavigator, - modifier: Modifier = Modifier, - topBarContent: @Composable () -> Unit = {}, - primaryContent: @Composable () -> Unit = {}, - secondaryContent: @Composable () -> Unit, - bottomNavContent: @Composable () -> Unit = {}, - snackbarHost: @Composable () -> Unit = {}, - contentWindowInsets: WindowInsets = WindowInsets.systemBars, - animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default -) { - val showDetail = navigator.scaffoldValue.primary == PaneAdaptedValue.Expanded - val coroutineScope = rememberCoroutineScope() - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val directionMultiplier = if (isRtl) -1 else 1 - val skipSlide = AppScaffoldNavigator.NavigationState.ENTER !in animatorFactory.enabledStates - - BackHandler(enabled = navigator.canNavigateBack()) { - coroutineScope.launch { navigator.navigateBack() } - } - - Scaffold( - containerColor = Color.Transparent, - contentWindowInsets = contentWindowInsets, - topBar = topBarContent, - snackbarHost = snackbarHost, - modifier = modifier - ) { paddingValues -> - AnimatedContent( - targetState = showDetail, - transitionSpec = { - val transform = when { - skipSlide -> EnterTransition.None togetherWith ExitTransition.None - targetState -> slideInHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> fullWidth * directionMultiplier } togetherWith - slideOutHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> -fullWidth * directionMultiplier } - else -> slideInHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> -fullWidth * directionMultiplier } togetherWith - slideOutHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> fullWidth * directionMultiplier } - } - transform using SizeTransform(clip = false) { _, _ -> snap() } - }, - modifier = Modifier.padding(paddingValues), - label = "SimpleAppScaffold" - ) { isDetail -> - if (isDetail) { - primaryContent() - } else { - Column(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier.weight(1f)) { - secondaryContent() - } - bottomNavContent() - } - } - } - } -} - -@Composable -private fun ListAndNavigation( - topBarContent: @Composable () -> Unit, - listContent: @Composable () -> Unit, - navRailContent: @Composable () -> Unit, - bottomNavContent: @Composable () -> Unit, - snackbarHost: @Composable () -> Unit = {}, - contentWindowInsets: WindowInsets, - modifier: Modifier = Modifier -) { - val navigationType = NavigationType.rememberNavigationType() - - Scaffold( - containerColor = Color.Transparent, - topBar = topBarContent, - contentWindowInsets = contentWindowInsets, - snackbarHost = snackbarHost, - modifier = modifier - ) { paddingValues -> - Row( - modifier = Modifier - .padding(paddingValues) - ) { - if (navigationType == NavigationType.RAIL) { - navRailContent() - } - - Column { - Box(modifier = Modifier.weight(1f)) { - listContent() - } - - if (navigationType == NavigationType.BAR) { - bottomNavContent() - } - } - } - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@BreakpointPreviews -@Composable -private fun AppScaffoldPreview() { - Previews.Preview { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - val isSplitPane = LocalResources.current.rememberIsSplitPane() - - AppScaffold( - navigator = rememberAppScaffoldNavigator( - isSplitPane = isSplitPane, - defaultPanePreferredWidth = 416.dp, - horizontalPartitionSpacerSize = 16.dp - ), - secondaryContent = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .background(color = Color.Red) - ) { - Text( - text = "ListContent\n$windowSizeClass", - textAlign = TextAlign.Center - ) - } - }, - primaryContent = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .background(color = Color.Blue) - ) { - Text( - text = "DetailContent", - textAlign = TextAlign.Center - ) - } - }, - navRailContent = { - MainNavigationRail( - state = MainNavigationBarState(), - mainFloatingActionButtonsCallback = MainFloatingActionButtonsCallback.Empty, - onDestinationSelected = {} - ) - }, - bottomNavContent = { - MainNavigationBar( - state = MainNavigationBarState(), - onDestinationSelected = {} - ) - }, - paneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle = { - AppPaneDragHandle( - paneExpansionState = it, - mutableInteractionSource = remember { MutableInteractionSource() } - ) - } - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldScope.AppPaneDragHandle( - paneExpansionState: PaneExpansionState, - mutableInteractionSource: MutableInteractionSource -) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .paneExpansionDraggable( - state = paneExpansionState, - minTouchTargetSize = LocalMinimumInteractiveComponentSize.current, - interactionSource = mutableInteractionSource, - semanticsProperties = paneExpansionState.defaultDragHandleSemantics() - ) - ) { - Box( - modifier = Modifier - .size(4.dp, 48.dp) - .background(color = Color(0xFF605F5D), RoundedCornerShape(percent = 50)) - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt deleted file mode 100644 index 39b31d3cd1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.window - -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.tween -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.GraphicsLayerScope -import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.coerceAtMost -import androidx.compose.ui.unit.dp - -/** - * Default animation settings for app-scaffold animations. - */ -object AppScaffoldAnimationDefaults { - val TweenEasing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f) - val InitAnimationOffset = 48.dp - - fun tween() = tween(durationMillis = 200, easing = TweenEasing) -} - -data class AppScaffoldAnimationState( - private val alpha: State = mutableStateOf(1f), - private val scale: State = mutableStateOf(1f), - val scaleMinimum: Float = 0f, - private val offset: State = mutableStateOf(0.dp), - private val corners: State = mutableStateOf(0.dp), - val cornersMaximum: Dp = 1000.dp, - private val parentOverlayAlpha: State = mutableStateOf(0f) -) { - - private val unclampedScale by scale - private val unclampedCorners by corners - - val contentAlpha by alpha - val contentScale by derivedStateOf { unclampedScale.coerceAtLeast(scaleMinimum) } - val contentOffset by offset - val contentCorners by derivedStateOf { unclampedCorners.coerceAtMost(cornersMaximum) } - - fun ContentDrawScope.applyParentValues() { - drawContent() - - drawRect(Color(0f, 0f, 0f, parentOverlayAlpha.value)) - } - - fun GraphicsLayerScope.applyChildValues() { - this.alpha = contentAlpha - this.scaleX = contentScale - this.scaleY = contentScale - this.translationX = contentOffset.toPx() - this.translationY = 0f - this.clip = true - this.shape = RoundedCornerShape(contentCorners) - } -} - -/** - * Allows for the customization of the AppScaffold Animators. - */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -class AppScaffoldAnimationStateFactory( - val enabledStates: Set = AppScaffoldNavigator.NavigationState.entries.toSet() -) { - - companion object { - val Default = AppScaffoldAnimationStateFactory() - - private val EMPTY_STATE = AppScaffoldAnimationState( - alpha = mutableStateOf(1f) - ) - } - - private var latestListSeekState: AppScaffoldAnimationState = EMPTY_STATE - private var latestDetailSeekState: AppScaffoldAnimationState = EMPTY_STATE - - @Composable - fun ThreePaneScaffoldPaneScope.getListAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState { - if (navigationState !in enabledStates) { - return EMPTY_STATE - } - - return when (navigationState) { - AppScaffoldNavigator.NavigationState.ENTER -> defaultListInitAnimationState() - AppScaffoldNavigator.NavigationState.EXIT -> defaultListInitAnimationState() - AppScaffoldNavigator.NavigationState.SEEK -> defaultListSeekAnimationState().also { - latestListSeekState = it - } - - AppScaffoldNavigator.NavigationState.RELEASE -> defaultListReleaseAnimationState(latestListSeekState) - } - } - - @Composable - fun ThreePaneScaffoldPaneScope.getDetailAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState { - if (navigationState !in enabledStates) { - return EMPTY_STATE - } - - return when (navigationState) { - AppScaffoldNavigator.NavigationState.ENTER -> defaultDetailInitAnimationState() - AppScaffoldNavigator.NavigationState.EXIT -> defaultDetailInitAnimationState() - AppScaffoldNavigator.NavigationState.SEEK -> defaultDetailSeekAnimationState().also { - latestDetailSeekState = it - } - - AppScaffoldNavigator.NavigationState.RELEASE -> defaultDetailReleaseAnimationState(latestDetailSeekState) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt deleted file mode 100644 index dc7851259d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.window - -import androidx.compose.animation.core.FiniteAnimationSpec -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.spring -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp - -private const val SEEK_DAMPING_RATIO = Spring.DampingRatioNoBouncy -private const val SEEK_STIFFNESS = Spring.StiffnessMedium - -/** - * Default animation spec for back gesture seeking. - */ -fun appScaffoldSeekSpring(): FiniteAnimationSpec = spring( - dampingRatio = SEEK_DAMPING_RATIO, - stiffness = SEEK_STIFFNESS -) - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.animateDp( - transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec = { AppScaffoldAnimationDefaults.tween() }, - targetWhenHiding: () -> Dp = { 0.dp }, - targetWhenShowing: () -> Dp -): State { - return scaffoldStateTransition.animateDp( - transitionSpec = transitionSpec - ) { - val isHiding = it[paneRole] == PaneAdaptedValue.Hidden - - if (isHiding) { - targetWhenHiding() - } else { - targetWhenShowing() - } - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.animateFloat( - transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec = { AppScaffoldAnimationDefaults.tween() }, - targetWhenHiding: () -> Float = { 0f }, - targetWhenShowing: () -> Float -): State { - return scaffoldStateTransition.animateFloat( - transitionSpec = transitionSpec - ) { - val isHiding = it[paneRole] == PaneAdaptedValue.Hidden - - if (isHiding) { - targetWhenHiding() - } else { - targetWhenShowing() - } - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnimationState { - val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1 - - val offset = animateDp( - targetWhenHiding = { - -AppScaffoldAnimationDefaults.InitAnimationOffset * directionMultiplier - }, - targetWhenShowing = { - 0.dp - } - ) - - val alpha = animateFloat { - 1f - } - - return remember { - AppScaffoldAnimationState( - alpha = alpha, - offset = offset - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnimationState { - val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1 - - val scale = animateFloat( - transitionSpec = { - appScaffoldSeekSpring() - }, - targetWhenShowing = { 0.5f }, - targetWhenHiding = { 1f } - ) - - val offset = animateDp( - transitionSpec = { - appScaffoldSeekSpring() - }, - targetWhenHiding = { -(88.dp) * directionMultiplier }, - targetWhenShowing = { 0.dp } - ) - - return remember { - AppScaffoldAnimationState( - offset = offset, - scale = scale, - scaleMinimum = 0.9f, - parentOverlayAlpha = mutableStateOf(0.2f) - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.defaultListReleaseAnimationState(from: AppScaffoldAnimationState): AppScaffoldAnimationState { - val initialScale = remember { from.contentScale } - val initialOffset = remember { from.contentOffset } - - val scale = animateFloat( - targetWhenHiding = { initialScale }, - targetWhenShowing = { 1f } - ) - - val offset = animateDp( - targetWhenHiding = { initialOffset }, - targetWhenShowing = { 0.dp } - ) - - val alpha = animateFloat( - targetWhenHiding = { 0.2f }, - targetWhenShowing = { 0f } - ) - - return remember { - AppScaffoldAnimationState( - scale = scale, - scaleMinimum = from.scaleMinimum, - offset = offset, - parentOverlayAlpha = alpha - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAnimationState { - val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1 - - val offset = animateDp( - targetWhenHiding = { - AppScaffoldAnimationDefaults.InitAnimationOffset * directionMultiplier - }, - targetWhenShowing = { - 0.dp - } - ) - - val alpha = animateFloat { - 1f - } - - return remember { - AppScaffoldAnimationState( - alpha = alpha, - offset = offset - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAnimationState { - val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1 - - val scale = animateFloat( - transitionSpec = { - appScaffoldSeekSpring() - }, - targetWhenShowing = { 1f }, - targetWhenHiding = { 0.5f } - ) - - val offset = animateDp( - transitionSpec = { - appScaffoldSeekSpring() - }, - targetWhenShowing = { 0.dp }, - targetWhenHiding = { 88.dp * directionMultiplier } - ) - - val roundedCorners = animateDp( - transitionSpec = { - appScaffoldSeekSpring() - } - ) { 1000.dp } - - return remember { - AppScaffoldAnimationState( - scale = scale, - scaleMinimum = 0.9f, - offset = offset, - corners = roundedCorners, - cornersMaximum = 42.dp - ) - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun ThreePaneScaffoldPaneScope.defaultDetailReleaseAnimationState(from: AppScaffoldAnimationState): AppScaffoldAnimationState { - val scale = remember { from.contentScale } - val offset = remember { from.contentOffset } - val corners = remember { from.contentCorners } - - val scaleState = remember { mutableStateOf(scale) } - val offsetState = remember { mutableStateOf(offset) } - val cornersState = remember { mutableStateOf(corners) } - - val alpha = animateFloat { 1f } - - return remember { - AppScaffoldAnimationState( - scale = scaleState, - scaleMinimum = from.scaleMinimum, - offset = offsetState, - corners = cornersState, - cornersMaximum = from.cornersMaximum, - alpha = alpha - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt deleted file mode 100644 index 76aa1a272b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.window - -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.annotation.RememberInComposition -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalResources -import androidx.compose.ui.unit.Dp -import androidx.window.core.layout.WindowSizeClass -import org.signal.core.ui.horizontalPartitionDefaultSpacerSize -import org.signal.core.ui.isSplitPane -import org.signal.core.ui.listPaneDefaultPreferredWidth -import org.signal.core.ui.rememberIsSplitPane - -/** - * AppScaffoldNavigator wraps a delegate navigator (such as the value returned by [rememberThreePaneScaffoldNavigatorDelegate] - * and implements a state machine that will produce [NavigationState] to allow proper animation coordination. - */ -@Stable -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -open class AppScaffoldNavigator @RememberInComposition constructor(private val delegate: ThreePaneScaffoldNavigator) : ThreePaneScaffoldNavigator by delegate { - - var state: NavigationState by mutableStateOf(NavigationState.ENTER) - private set - - override suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) { - state = NavigationState.ENTER - return delegate.navigateTo(pane, contentKey) - } - - override suspend fun navigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean { - if (state == NavigationState.SEEK) { - state = NavigationState.RELEASE - } - - if (state == NavigationState.ENTER) { - state = NavigationState.EXIT - } - - return delegate.navigateBack(backNavigationBehavior) - } - - override suspend fun seekBack(backNavigationBehavior: BackNavigationBehavior, fraction: Float) { - if (fraction == 0f && state == NavigationState.SEEK) { - state = NavigationState.ENTER - } - - if (fraction > 0f && state != NavigationState.SEEK) { - state = NavigationState.SEEK - } - - return delegate.seekBack(backNavigationBehavior, fraction) - } - - /** - * State machine which describes the current navigation state to help with animation coordination. - */ - enum class NavigationState { - /** - * We've navigated to a new pane. - */ - ENTER, - - /** - * We've navigated back from a pane without using seek. - */ - EXIT, - - /** - * The user is performing a back gesture seek action. - */ - SEEK, - - /** - * The user has let go of a seek and will go back. - */ - RELEASE - } -} - -/** - * Sane default navigator. If you want to provide your own implementation - * of AppScaffoldNavigator, utilize the remember pattern here but use - * [rememberThreePaneScaffoldNavigatorDelegate] to get a delegate and hand off - * to your own subclass of [AppScaffoldNavigator] - */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun rememberAppScaffoldNavigator( - windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass, - isSplitPane: Boolean = LocalResources.current.rememberIsSplitPane(), - horizontalPartitionSpacerSize: Dp = windowSizeClass.horizontalPartitionDefaultSpacerSize, - defaultPanePreferredWidth: Dp = windowSizeClass.listPaneDefaultPreferredWidth -): AppScaffoldNavigator { - val delegate = rememberThreePaneScaffoldNavigatorDelegate( - isSplitPane, - horizontalPartitionSpacerSize, - defaultPanePreferredWidth - ) - - return remember(delegate) { AppScaffoldNavigator(delegate) } -} - -/** - * Produces a ThreePaneScaffoldNavigatorDelegate. Since the developer can - * further modify navigator behavior, this is best done using the delegate pattern. - * Use this to grab the initial delegate, and then either subclass or create an - * instance of [AppScaffoldNavigator] keyed to this delegate. - */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun rememberThreePaneScaffoldNavigatorDelegate( - isSplitPane: Boolean, - horizontalPartitionSpacerSize: Dp, - defaultPanePreferredWidth: Dp -): ThreePaneScaffoldNavigator { - return rememberListDetailPaneScaffoldNavigator( - scaffoldDirective = calculatePaneScaffoldDirective( - currentWindowAdaptiveInfo() - ).copy( - maxHorizontalPartitions = if (isSplitPane) 2 else 1, - horizontalPartitionSpacerSize = horizontalPartitionSpacerSize, - defaultPanePreferredWidth = defaultPanePreferredWidth - ) - ) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldWithTopBar.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldWithTopBar.kt deleted file mode 100644 index ce2250971a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldWithTopBar.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.window - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -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.text.style.TextAlign -import org.signal.core.ui.compose.BreakpointPreviews -import org.signal.core.ui.compose.Previews -import org.signal.core.ui.compose.Scaffolds -import org.signal.core.ui.compose.SignalIcons - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) -@BreakpointPreviews -@Composable -private fun AppScaffoldWithTopBarPreview() { - Previews.Preview { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - - AppScaffold( - navigator = rememberAppScaffoldNavigator(), - - topBarContent = { - Scaffolds.DefaultTopAppBar( - title = "Hello World!", - titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) }, - navigationIcon = SignalIcons.ArrowStart.imageVector, - navigationContentDescription = "", - onNavigationClick = { } - ) - }, - - secondaryContent = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .background(color = Color.Red) - ) { - Text( - text = "ListContent\n$windowSizeClass", - textAlign = TextAlign.Center - ) - } - }, - - primaryContent = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .background(color = Color.Blue) - ) { - Text( - text = "DetailContent", - textAlign = TextAlign.Center - ) - } - } - ) - } -} diff --git a/core/ui/src/main/java/org/signal/core/ui/navigation/TransitionSpecs.kt b/core/ui/src/main/java/org/signal/core/ui/navigation/TransitionSpecs.kt index 19e6f67b74..1b5ea0414a 100644 --- a/core/ui/src/main/java/org/signal/core/ui/navigation/TransitionSpecs.kt +++ b/core/ui/src/main/java/org/signal/core/ui/navigation/TransitionSpecs.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.ContentTransform import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -36,9 +37,18 @@ import androidx.navigation3.ui.NavDisplay object TransitionSpecs { private const val PANE_SHIFT_DURATION = 200 - private val PANE_SHIFT_OFFSET = 48.dp private val PANE_SHIFT_EASING = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f) + /** + * The distance content travels during a [paneShift]. + */ + val PANE_SHIFT_OFFSET = 48.dp + + /** + * The spec a [paneShift] animates with. + */ + fun paneShiftSpec(): FiniteAnimationSpec = tween(durationMillis = PANE_SHIFT_DURATION, easing = PANE_SHIFT_EASING) + interface Transition { companion object { val NONE: ContentTransform = EnterTransition.None togetherWith ExitTransition.None @@ -80,7 +90,7 @@ object TransitionSpecs { * rather than a whole width, cross-fading as it goes, with no scale. * * A function rather than a [Transition] value because the distance is fixed in dp, so it needs a - * [Density]. Values mirror `AppScaffoldAnimationDefaults`, which cannot be referenced from here. + * [Density]. * * @param pop reverses the direction, for navigating back. */ @@ -88,8 +98,8 @@ object TransitionSpecs { val offset = with(density) { PANE_SHIFT_OFFSET.roundToPx() } val direction = if (layoutDirection == LayoutDirection.Rtl) -1 else 1 val sign = if (pop) -1 else 1 - val slideSpec = tween(durationMillis = PANE_SHIFT_DURATION, easing = PANE_SHIFT_EASING) - val fadeSpec = tween(durationMillis = PANE_SHIFT_DURATION, easing = PANE_SHIFT_EASING) + val slideSpec = paneShiftSpec() + val fadeSpec = paneShiftSpec() return slideInHorizontally(animationSpec = slideSpec) { offset * sign * direction } + fadeIn(animationSpec = fadeSpec) togetherWith slideOutHorizontally(animationSpec = slideSpec) { -offset * sign * direction } + fadeOut(animationSpec = fadeSpec)