mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Add "fake" chat list bitmap to fake transition.
This commit is contained in:
committed by
Greyson Parrelli
parent
bd25447a8f
commit
d4c266561f
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Boolean> = 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)
|
||||
|
||||
@@ -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<MainNavigationDetailLocation.Empty> {
|
||||
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<Boolean> = 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<Boolean>.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<Boolean>.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ fun EmptyDetailScreen() {
|
||||
*/
|
||||
@Composable
|
||||
fun rememberMainNavigationDetailLocation(
|
||||
mainNavigationViewModel: MainNavigationViewModel
|
||||
mainNavigationViewModel: MainNavigationViewModel,
|
||||
onWillFocusPrimary: suspend () -> Unit = {}
|
||||
): State<MainNavigationDetailLocation> {
|
||||
val state = rememberSaveable(
|
||||
stateSaver = MainNavigationDetailLocation.Saver()
|
||||
@@ -75,6 +76,7 @@ fun rememberMainNavigationDetailLocation(
|
||||
if (it == MainNavigationDetailLocation.Empty) {
|
||||
ThreePaneScaffoldRole.Secondary
|
||||
} else {
|
||||
onWillFocusPrimary()
|
||||
ThreePaneScaffoldRole.Primary
|
||||
}
|
||||
)
|
||||
|
||||
@@ -221,6 +221,7 @@ enum class WindowSizeClass(
|
||||
@Composable
|
||||
fun AppScaffold(
|
||||
navigator: AppScaffoldNavigator<Any>,
|
||||
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
|
||||
|
||||
@@ -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 <T> tween() = tween<T>(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> = 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
|
||||
}
|
||||
|
||||
@@ -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 <T> appScaffoldSeekSpring(): FiniteAnimationSpec<T> = 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<Dp> = { tween(durationMillis = 200, easing = easing) },
|
||||
transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec<Dp> = { AppScaffoldAnimationDefaults.tween() },
|
||||
targetWhenHiding: () -> Dp = { 0.dp },
|
||||
targetWhenShowing: () -> Dp
|
||||
): State<Dp> {
|
||||
@@ -63,7 +59,7 @@ fun ThreePaneScaffoldPaneScope.animateDp(
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.animateFloat(
|
||||
transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec<Float> = { tween(durationMillis = 200, easing = easing) },
|
||||
transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec<Float> = { AppScaffoldAnimationDefaults.tween() },
|
||||
targetWhenHiding: () -> Float = { 0f },
|
||||
targetWhenShowing: () -> Float
|
||||
): State<Float> {
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -31,11 +31,11 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
open class AppScaffoldNavigator<T> @RememberInComposition constructor(private val delegate: ThreePaneScaffoldNavigator<T>) : ThreePaneScaffoldNavigator<T> 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<T> @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<T> @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.
|
||||
|
||||
Reference in New Issue
Block a user