From d4c266561febbc0bfa33b31d36d3ba4b74202062 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 22 Oct 2025 10:50:27 -0300 Subject: [PATCH] Add "fake" chat list bitmap to fake transition. --- .../thoughtcrime/securesms/MainActivity.kt | 32 +++- .../conversation/v2/ConversationFragment.kt | 7 + .../securesms/main/ChatsNavHost.kt | 150 +++++++++++++++++- .../securesms/main/MainActivityComponents.kt | 4 +- .../securesms/window/AppScaffold.kt | 13 +- .../window/AppScaffoldAnimationState.kt | 43 ++++- .../securesms/window/AppScaffoldAnimators.kt | 16 +- .../securesms/window/AppScaffoldNavigator.kt | 19 ++- 8 files changed, 252 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index 1357d6023f..0900e95a48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -106,6 +106,7 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity +import org.thoughtcrime.securesms.main.ChatNavGraphState import org.thoughtcrime.securesms.main.DetailsScreenNavHost import org.thoughtcrime.securesms.main.MainBottomChrome import org.thoughtcrime.securesms.main.MainBottomChromeCallback @@ -162,6 +163,7 @@ import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.window.AppPaneDragHandle import org.thoughtcrime.securesms.window.AppScaffold +import org.thoughtcrime.securesms.window.AppScaffoldAnimationStateFactory import org.thoughtcrime.securesms.window.AppScaffoldNavigator import org.thoughtcrime.securesms.window.WindowSizeClass import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate @@ -367,8 +369,9 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner anchors = listOf(detailOnlyAnchor, detailAndListAnchor, listOnlyAnchor) ) + val chatNavGraphState = ChatNavGraphState.remember(windowSizeClass) val mutableInteractionSource = remember { MutableInteractionSource() } - val mainNavigationDetailLocation by rememberMainNavigationDetailLocation(mainNavigationViewModel) + val mainNavigationDetailLocation by rememberMainNavigationDetailLocation(mainNavigationViewModel, chatNavGraphState::writeGraphicsLayerToBitmap) val chatsNavHostController = rememberDetailNavHostController( onRequestFocus = rememberFocusRequester( @@ -377,7 +380,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner isTargetListLocation = { it in listOf(MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE) } ) ) { - chatNavGraphBuilder() + chatNavGraphBuilder(chatNavGraphState) } val callsNavHostController = rememberDetailNavHostController( @@ -409,7 +412,11 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner }.navigateToDetailLocation(mainNavigationDetailLocation) } - is MainNavigationDetailLocation.Chats -> chatsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation) + is MainNavigationDetailLocation.Chats -> { + chatNavGraphState.writeGraphicsLayerToBitmap() + chatsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation) + } + is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation) is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(mainNavigationDetailLocation) } @@ -427,6 +434,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner listOnlyAnchor, detailOnlyAnchor -> { true } + else -> { false } @@ -454,8 +462,17 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } } + val noEnterTransitionFactory = remember { + AppScaffoldAnimationStateFactory( + enabledStates = AppScaffoldNavigator.NavigationState.entries.filterNot { + it == AppScaffoldNavigator.NavigationState.ENTER + }.toSet() + ) + } + AppScaffold( navigator = wrappedNavigator, + modifier = chatNavGraphState.writeContentToGraphicsLayer(), paneExpansionState = paneExpansionState, contentWindowInsets = WindowInsets(), bottomNavContent = { @@ -585,7 +602,14 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner mutableInteractionSource = mutableInteractionSource ) } - } else null + } else { + null + }, + animatorFactory = if (mainNavigationState.currentListLocation == MainNavigationListLocation.CHATS || mainNavigationState.currentListLocation == MainNavigationListLocation.ARCHIVE) { + noEnterTransitionFactory + } else { + AppScaffoldAnimationStateFactory.Default + } ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index ba641d26bf..9679315421 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -87,9 +87,12 @@ import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -596,6 +599,9 @@ class ConversationFragment : private lateinit var voiceMessageRecordingDelegate: VoiceMessageRecordingDelegate + private val internalDidFirstFrameRender = MutableStateFlow(false) + val didFirstFrameRender: StateFlow = internalDidFirstFrameRender + //region Android Lifecycle override fun onCreate(savedInstanceState: Bundle?) { @@ -980,6 +986,7 @@ class ConversationFragment : } activity?.supportStartPostponedEnterTransition() + internalDidFirstFrameRender.update { true } val backPressedDelegate = BackPressedDelegate() requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedDelegate) diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt index 99aa3c9f37..4ad7df9c78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt @@ -5,14 +5,31 @@ package org.thoughtcrime.securesms.main +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.rememberFragmentState import androidx.lifecycle.Lifecycle @@ -21,14 +38,23 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.thoughtcrime.securesms.conversation.ConversationArgs import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.v2.ConversationFragment import org.thoughtcrime.securesms.serialization.JsonSerializableNavType +import org.thoughtcrime.securesms.window.AppScaffoldAnimationDefaults +import org.thoughtcrime.securesms.window.AppScaffoldAnimationState +import org.thoughtcrime.securesms.window.AppScaffoldNavigator +import org.thoughtcrime.securesms.window.WindowSizeClass import kotlin.reflect.typeOf +import kotlin.time.Duration.Companion.milliseconds -fun NavGraphBuilder.chatNavGraphBuilder() { +fun NavGraphBuilder.chatNavGraphBuilder( + chatNavGraphState: ChatNavGraphState +) { composable { EmptyDetailScreen() } @@ -44,11 +70,38 @@ fun NavGraphBuilder.chatNavGraphBuilder() { val insets by rememberVerticalInsets() val insetFlow = remember { snapshotFlow { insets } } + // Because it can take a long time to load content, we use a "fake" chat list image to delay displaying + // the fragment and prevent pop-in + var shouldDisplayFragment by remember { mutableStateOf(false) } + val transition: Transition = updateTransition(shouldDisplayFragment) + val bitmap = chatNavGraphState.chatBitmap + + val fakeChatListAnimationState = transition.fakeChatListAnimationState() + val chatAnimationState = transition.chatAnimationState(bitmap != null) + + LaunchedEffect(transition.currentState, transition.isRunning) { + if (transition.currentState && !transition.isRunning) { + chatNavGraphState.clearBitmap() + } + } + + if (bitmap != null) { + Image( + bitmap = bitmap, + contentDescription = null, + modifier = Modifier + .then(fakeChatListAnimationState.toModifier()) + .fillMaxSize() + ) + } + AndroidFragment( clazz = ConversationFragment::class.java, fragmentState = fragmentState, arguments = requireNotNull(ConversationIntents.createBuilderSync(context, route.conversationArgs).build().extras) { "Handed null Conversation intent arguments." }, modifier = Modifier + .then(chatAnimationState.toModifier()) + .background(MaterialTheme.colorScheme.background) .fillMaxSize() ) { fragment -> fragment.viewLifecycleOwner.lifecycleScope.launch { @@ -58,6 +111,101 @@ fun NavGraphBuilder.chatNavGraphBuilder() { } } } + + fragment.viewLifecycleOwner.lifecycleScope.launch { + fragment.repeatOnLifecycle(Lifecycle.State.STARTED) { + fragment.didFirstFrameRender.collectLatest { + shouldDisplayFragment = it + if (!it) { + delay(150.milliseconds) + shouldDisplayFragment = true + } + } + } + } } } } + +@Composable +private fun Transition.fakeChatListAnimationState(): AppScaffoldAnimationState { + val alpha by animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0f else 1f } + val offset by animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) (-48).dp else 0.dp } + + return AppScaffoldAnimationState( + navigationState = AppScaffoldNavigator.NavigationState.ENTER, + offset = offset, + alpha = alpha + ) +} + +@Composable +private fun Transition.chatAnimationState(hasFake: Boolean): AppScaffoldAnimationState { + val alpha by animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 1f else 0f } + + return if (!hasFake) { + AppScaffoldAnimationState( + navigationState = AppScaffoldNavigator.NavigationState.ENTER, + offset = 0.dp, + alpha = alpha + ) + } else { + val offset by animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0.dp else 48.dp } + + AppScaffoldAnimationState( + navigationState = AppScaffoldNavigator.NavigationState.ENTER, + offset = offset, + alpha = alpha + ) + } +} + +/** + * Allows the setting of a "fake" bitmap driven by a graphics layer to coordinate delayed animations + * in lieu of proper support for postponing enter transitions. + */ +@Stable +class ChatNavGraphState private constructor( + val windowSizeClass: WindowSizeClass, + val graphicsLayer: GraphicsLayer +) { + companion object { + @Composable + fun remember(windowSizeClass: WindowSizeClass): ChatNavGraphState { + val graphicsLayer = rememberGraphicsLayer() + + return remember(windowSizeClass) { + ChatNavGraphState( + windowSizeClass, + graphicsLayer + ) + } + } + } + + var chatBitmap: ImageBitmap? by mutableStateOf(null) + private set + + private var hasWrittenToGraphicsLayer: Boolean by mutableStateOf(false) + + suspend fun writeGraphicsLayerToBitmap() { + if (WindowSizeClass.isLargeScreenSupportEnabled() && !windowSizeClass.isSplitPane() && hasWrittenToGraphicsLayer) { + chatBitmap = graphicsLayer.toImageBitmap() + } + } + + fun writeContentToGraphicsLayer(): Modifier { + return Modifier.drawWithContent { + graphicsLayer.record { + this@drawWithContent.drawContent() + hasWrittenToGraphicsLayer = true + } + + drawLayer(graphicsLayer) + } + } + + fun clearBitmap() { + chatBitmap = null + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityComponents.kt index 6fac8860f4..ede75b0994 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityComponents.kt @@ -60,7 +60,8 @@ fun EmptyDetailScreen() { */ @Composable fun rememberMainNavigationDetailLocation( - mainNavigationViewModel: MainNavigationViewModel + mainNavigationViewModel: MainNavigationViewModel, + onWillFocusPrimary: suspend () -> Unit = {} ): State { val state = rememberSaveable( stateSaver = MainNavigationDetailLocation.Saver() @@ -75,6 +76,7 @@ fun rememberMainNavigationDetailLocation( if (it == MainNavigationDetailLocation.Empty) { ThreePaneScaffoldRole.Secondary } else { + onWillFocusPrimary() ThreePaneScaffoldRole.Primary } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt index 48036bef2d..002e6ee941 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt @@ -221,6 +221,7 @@ enum class WindowSizeClass( @Composable fun AppScaffold( navigator: AppScaffoldNavigator, + modifier: Modifier = Modifier, topBarContent: @Composable () -> Unit = {}, primaryContent: @Composable () -> Unit = {}, secondaryContent: @Composable () -> Unit, @@ -242,7 +243,8 @@ fun AppScaffold( navRailContent = navRailContent, bottomNavContent = bottomNavContent, windowSizeClass = windowSizeClass, - contentWindowInsets = contentWindowInsets + contentWindowInsets = contentWindowInsets, + modifier = modifier ) return @@ -255,7 +257,8 @@ fun AppScaffold( containerColor = Color.Transparent, contentWindowInsets = contentWindowInsets, topBar = topBarContent, - snackbarHost = snackbarHost + snackbarHost = snackbarHost, + modifier = modifier ) { paddingValues -> NavigableListDetailPaneScaffold( navigator = navigator, @@ -353,13 +356,15 @@ private fun ListAndNavigation( bottomNavContent: @Composable () -> Unit, snackbarHost: @Composable () -> Unit = {}, windowSizeClass: WindowSizeClass, - contentWindowInsets: WindowInsets + contentWindowInsets: WindowInsets, + modifier: Modifier = Modifier ) { Scaffold( containerColor = Color.Transparent, topBar = topBarContent, contentWindowInsets = contentWindowInsets, - snackbarHost = snackbarHost + snackbarHost = snackbarHost, + modifier = modifier ) { paddingValues -> Row( modifier = Modifier diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt index 6a6cec481c..4e2d11f0e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimationState.kt @@ -5,6 +5,8 @@ package org.thoughtcrime.securesms.window +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.offset import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @@ -18,6 +20,16 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp 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) +} + /** * Produces modifier that can be composed into another modifier chain. * This object allows us to store "latest state" as we transition. @@ -47,17 +59,31 @@ data class AppScaffoldAnimationState( * Allows for the customization of the AppScaffold Animators. */ @OptIn(ExperimentalMaterial3AdaptiveApi::class) -abstract class AppScaffoldAnimationStateFactory { +class AppScaffoldAnimationStateFactory( + val enabledStates: Set = AppScaffoldNavigator.NavigationState.entries.toSet() +) { - object Default : AppScaffoldAnimationStateFactory() + companion object { + val Default = AppScaffoldAnimationStateFactory() - protected var latestListSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK) - protected var latestDetailSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK) + private val EMPTY_STATE = AppScaffoldAnimationState( + navigationState = AppScaffoldNavigator.NavigationState.ENTER, + alpha = 1f + ) + } + + private var latestListSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK) + private var latestDetailSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK) @Composable fun ThreePaneScaffoldPaneScope.getListAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState { + if (navigationState !in enabledStates) { + return EMPTY_STATE + } + return when (navigationState) { - AppScaffoldNavigator.NavigationState.INIT -> defaultListInitAnimationState() + AppScaffoldNavigator.NavigationState.ENTER -> defaultListInitAnimationState() + AppScaffoldNavigator.NavigationState.EXIT -> defaultListInitAnimationState() AppScaffoldNavigator.NavigationState.SEEK -> defaultListSeekAnimationState().also { latestListSeekState = it } @@ -67,8 +93,13 @@ abstract class AppScaffoldAnimationStateFactory { @Composable fun ThreePaneScaffoldPaneScope.getDetailAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState { + if (navigationState !in enabledStates) { + return EMPTY_STATE + } + return when (navigationState) { - AppScaffoldNavigator.NavigationState.INIT -> defaultDetailInitAnimationState() + AppScaffoldNavigator.NavigationState.ENTER -> defaultDetailInitAnimationState() + AppScaffoldNavigator.NavigationState.EXIT -> defaultDetailInitAnimationState() AppScaffoldNavigator.NavigationState.SEEK -> defaultDetailSeekAnimationState().also { latestDetailSeekState = it } diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt index 908afad74f..5c358c46e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt @@ -5,14 +5,12 @@ package org.thoughtcrime.securesms.window -import androidx.compose.animation.core.CubicBezierEasing 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.animation.core.tween import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.layout.PaneAdaptedValue @@ -38,12 +36,10 @@ fun appScaffoldSeekSpring(): FiniteAnimationSpec = spring( stiffness = SEEK_STIFFNESS ) -private val easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f) - @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun ThreePaneScaffoldPaneScope.animateDp( - transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec = { tween(durationMillis = 200, easing = easing) }, + transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec = { AppScaffoldAnimationDefaults.tween() }, targetWhenHiding: () -> Dp = { 0.dp }, targetWhenShowing: () -> Dp ): State { @@ -63,7 +59,7 @@ fun ThreePaneScaffoldPaneScope.animateDp( @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun ThreePaneScaffoldPaneScope.animateFloat( - transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec = { tween(durationMillis = 200, easing = easing) }, + transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec = { AppScaffoldAnimationDefaults.tween() }, targetWhenHiding: () -> Float = { 0f }, targetWhenShowing: () -> Float ): State { @@ -85,7 +81,7 @@ fun ThreePaneScaffoldPaneScope.animateFloat( fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnimationState { val offset by animateDp( targetWhenHiding = { - (-48).dp + -AppScaffoldAnimationDefaults.InitAnimationOffset }, targetWhenShowing = { 0.dp @@ -97,7 +93,7 @@ fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnima } return AppScaffoldAnimationState( - AppScaffoldNavigator.NavigationState.INIT, + AppScaffoldNavigator.NavigationState.ENTER, alpha = alpha, offset = offset ) @@ -169,7 +165,7 @@ fun ThreePaneScaffoldPaneScope.defaultListReleaseAnimationState(from: AppScaffol fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAnimationState { val offset by animateDp( targetWhenHiding = { - 48.dp + AppScaffoldAnimationDefaults.InitAnimationOffset }, targetWhenShowing = { 0.dp @@ -181,7 +177,7 @@ fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAni } return AppScaffoldAnimationState( - navigationState = AppScaffoldNavigator.NavigationState.INIT, + navigationState = AppScaffoldNavigator.NavigationState.ENTER, alpha = alpha, offset = offset ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt index e59efe3dd3..e4b0e5f315 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldNavigator.kt @@ -31,11 +31,11 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore @OptIn(ExperimentalMaterial3AdaptiveApi::class) open class AppScaffoldNavigator @RememberInComposition constructor(private val delegate: ThreePaneScaffoldNavigator) : ThreePaneScaffoldNavigator by delegate { - var state: NavigationState by mutableStateOf(NavigationState.INIT) + var state: NavigationState by mutableStateOf(NavigationState.ENTER) private set override suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) { - state = NavigationState.INIT + state = NavigationState.ENTER return delegate.navigateTo(pane, contentKey) } @@ -44,6 +44,10 @@ open class AppScaffoldNavigator @RememberInComposition constructor(private va state = NavigationState.RELEASE } + if (state == NavigationState.ENTER) { + state = NavigationState.EXIT + } + return delegate.navigateBack(backNavigationBehavior) } @@ -60,11 +64,14 @@ open class AppScaffoldNavigator @RememberInComposition constructor(private va */ enum class NavigationState { /** - * We've navigated to a new pane. This animation is used for both immediate - * pane entry and exit (such as tapping a back button instead of using a - * gesture) + * We've navigated to a new pane. */ - INIT, + ENTER, + + /** + * We've navigated back from a pane without using seek. + */ + EXIT, /** * The user is performing a back gesture seek action.