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 4f3693a4b0..048a58001a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt @@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.window import android.content.res.Configuration import android.content.res.Resources +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -14,6 +16,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalMinimumInteractiveComponentSize @@ -30,9 +33,11 @@ import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneSca import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.layout @@ -42,6 +47,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.window.core.ExperimentalWindowCoreApi import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowWidthSizeClass @@ -228,9 +234,28 @@ fun AppScaffold( NavigableListDetailPaneScaffold( navigator = navigator, listPane = { - AnimatedPane { + val offset by animateDp( + targetWhenHiding = { + (-48).dp + }, + targetWhenShowing = { + 0.dp + } + ) + + val alpha by animateFloat { + 1f + } + + AnimatedPane( + enterTransition = EnterTransition.None, + exitTransition = ExitTransition.None, + modifier = Modifier.zIndex(0f) + ) { Box( modifier = Modifier + .alpha(alpha) + .offset(x = offset) .clipToBounds() .layout { measurable, constraints -> val width = max(minPaneWidth.roundToPx(), constraints.maxWidth) @@ -258,9 +283,28 @@ fun AppScaffold( } }, detailPane = { - AnimatedPane { + val offset by animateDp( + targetWhenHiding = { + 48.dp + }, + targetWhenShowing = { + 0.dp + } + ) + + val alpha by animateFloat { + 1f + } + + AnimatedPane( + enterTransition = EnterTransition.None, + exitTransition = ExitTransition.None, + modifier = Modifier.zIndex(1f) + ) { Box( modifier = Modifier + .alpha(alpha) + .offset(x = offset) .clipToBounds() .layout { measurable, constraints -> val width = max(minPaneWidth.roundToPx(), constraints.maxWidth) diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt new file mode 100644 index 0000000000..bdb704a68d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffoldAnimators.kt @@ -0,0 +1,58 @@ +/* + * 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.animateDp +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween +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.ui.unit.Dp +import androidx.compose.ui.unit.dp + +private val easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f) + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun ThreePaneScaffoldPaneScope.animateDp( + targetWhenHiding: () -> Dp = { 0.dp }, + targetWhenShowing: () -> Dp +): State { + return scaffoldStateTransition.animateDp( + transitionSpec = { tween(durationMillis = 200, easing = easing) } + ) { + val isHiding = it[paneRole] == PaneAdaptedValue.Hidden + + if (isHiding) { + targetWhenHiding() + } else { + targetWhenShowing() + } + } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun ThreePaneScaffoldPaneScope.animateFloat( + targetWhenHiding: () -> Float = { 0f }, + targetWhenShowing: () -> Float +): State { + return scaffoldStateTransition.animateFloat( + transitionSpec = { tween(durationMillis = 200, easing = easing) } + ) { + val isHiding = it[paneRole] == PaneAdaptedValue.Hidden + + if (isHiding) { + targetWhenHiding() + } else { + targetWhenShowing() + } + } +}