Wire up nav rail fabs and fix animation playing on leaving a tab.

This commit is contained in:
Alex Hart
2025-04-17 13:14:50 -03:00
committed by Cody Henthorne
parent 8b7b184224
commit c5e795b176
5 changed files with 170 additions and 90 deletions

View File

@@ -268,6 +268,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
if (isNavigationVisible) {
MainNavigationRail(
state = mainNavigationState,
mainFloatingActionButtonsCallback = mainBottomChromeCallback,
onDestinationSelected = mainNavigationCallback
)
}

View File

@@ -43,10 +43,7 @@ data class SnackbarState(
)
}
interface MainBottomChromeCallback {
fun onNewChatClick()
fun onNewCallClick()
fun onCameraClick(destination: MainNavigationListLocation)
interface MainBottomChromeCallback : MainFloatingActionButtonsCallback {
fun onMegaphoneVisible(megaphone: Megaphone)
fun onSnackbarDismissed()
@@ -79,21 +76,21 @@ fun MainBottomChrome(
megaphoneActionController: MegaphoneActionController,
modifier: Modifier = Modifier
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
Column(
modifier = modifier
.fillMaxWidth()
.animateContentSize()
) {
if (state.mainToolbarMode == MainToolbarMode.FULL) {
if (state.mainToolbarMode == MainToolbarMode.FULL && windowSizeClass.isCompact()) {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
) {
MainFloatingActionButtons(
destination = state.destination,
onCameraClick = callback::onCameraClick,
onNewCallClick = callback::onNewCallClick,
onNewChatClick = callback::onNewChatClick
callback = callback
)
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.animation.core.animateDp
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -20,6 +21,7 @@ import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -38,69 +40,129 @@ import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.window.Navigation
import org.thoughtcrime.securesms.window.WindowSizeClass
import kotlin.math.roundToInt
private val ACTION_BUTTON_SIZE = 56.dp
private val ACTION_BUTTON_SPACING = 16.dp
interface MainFloatingActionButtonsCallback {
fun onNewChatClick()
fun onNewCallClick()
fun onCameraClick(destination: MainNavigationListLocation)
object Empty : MainFloatingActionButtonsCallback {
override fun onNewChatClick() = Unit
override fun onNewCallClick() = Unit
override fun onCameraClick(destination: MainNavigationListLocation) = Unit
}
}
@Composable
fun MainFloatingActionButtons(
destination: MainNavigationListLocation,
onNewChatClick: () -> Unit = {},
onCameraClick: (MainNavigationListLocation) -> Unit = {},
onNewCallClick: () -> Unit = {}
callback: MainFloatingActionButtonsCallback,
modifier: Modifier = Modifier,
navigation: Navigation = Navigation.rememberNavigation()
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
if (windowSizeClass.navigation == Navigation.RAIL) {
return
}
val boxHeightDp = (ACTION_BUTTON_SIZE * 2 + ACTION_BUTTON_SPACING)
val boxHeightPx = with(LocalDensity.current) {
boxHeightDp.toPx().roundToInt()
}
val primaryButtonAlignment = remember(navigation) {
when (navigation) {
Navigation.RAIL -> Alignment.TopCenter
Navigation.BAR -> Alignment.BottomCenter
}
}
val shadowElevation: Dp = remember(navigation) {
when (navigation) {
Navigation.RAIL -> 0.dp
Navigation.BAR -> 4.dp
}
}
Box(
modifier = Modifier
modifier = modifier
.padding(ACTION_BUTTON_SPACING)
.height(boxHeightDp)
) {
AnimatedVisibility(
visible = destination == MainNavigationListLocation.CHATS,
modifier = Modifier.align(Alignment.TopCenter),
enter = slideInVertically(initialOffsetY = { boxHeightPx - it }),
exit = slideOutVertically(targetOffsetY = { boxHeightPx - it })
) {
val elevation by transition.animateDp(targetValueByState = { if (it == EnterExitState.Visible) 4.dp else 0.dp })
CameraButton(
colors = IconButtonDefaults.filledTonalIconButtonColors().copy(
containerColor = SignalTheme.colors.colorSurface1
),
onClick = {
onCameraClick(MainNavigationListLocation.CHATS)
},
shadowElevation = elevation
)
}
SecondaryActionButton(
destination = destination,
boxHeightPx = boxHeightPx,
onCameraClick = callback::onCameraClick,
elevation = shadowElevation
)
Box(
modifier = Modifier.align(Alignment.BottomCenter)
modifier = Modifier.align(primaryButtonAlignment)
) {
PrimaryActionButton(
destination = destination,
onNewChatClick = onNewChatClick,
onCameraClick = onCameraClick,
onNewCallClick = onNewCallClick
onNewChatClick = callback::onNewChatClick,
onCameraClick = callback::onCameraClick,
onNewCallClick = callback::onNewCallClick,
elevation = shadowElevation
)
}
}
}
@Composable
private fun BoxScope.SecondaryActionButton(
destination: MainNavigationListLocation,
boxHeightPx: Int,
elevation: Dp,
onCameraClick: (MainNavigationListLocation) -> Unit
) {
val navigation = Navigation.rememberNavigation()
val secondaryButtonAlignment = remember(navigation) {
when (navigation) {
Navigation.RAIL -> Alignment.BottomCenter
Navigation.BAR -> Alignment.TopCenter
}
}
val offsetYProvider: (Int) -> Int = remember(navigation) {
when (navigation) {
Navigation.RAIL -> {
{ it - boxHeightPx }
}
Navigation.BAR -> {
{ boxHeightPx - it }
}
}
}
AnimatedVisibility(
visible = destination == MainNavigationListLocation.CHATS,
modifier = Modifier.align(secondaryButtonAlignment),
enter = slideInVertically(initialOffsetY = offsetYProvider),
exit = slideOutVertically(targetOffsetY = offsetYProvider)
) {
val animatedElevation by transition.animateDp(targetValueByState = { if (it == EnterExitState.Visible) elevation else 0.dp })
CameraButton(
colors = IconButtonDefaults.filledTonalIconButtonColors().copy(
containerColor = when (navigation) {
Navigation.RAIL -> MaterialTheme.colorScheme.surface
Navigation.BAR -> SignalTheme.colors.colorSurface2
},
contentColor = MaterialTheme.colorScheme.onSurface
),
onClick = {
onCameraClick(MainNavigationListLocation.CHATS)
},
shadowElevation = animatedElevation
)
}
}
@Composable
private fun PrimaryActionButton(
destination: MainNavigationListLocation,
elevation: Dp,
onNewChatClick: () -> Unit = {},
onCameraClick: (MainNavigationListLocation) -> Unit = {},
onNewCallClick: () -> Unit = {}
@@ -117,6 +179,7 @@ private fun PrimaryActionButton(
MainFloatingActionButton(
onClick = onClick,
shadowElevation = elevation,
icon = {
AnimatedContent(destination) { targetState ->
val icon = when (targetState) {
@@ -178,15 +241,58 @@ private fun MainFloatingActionButton(
@SignalPreview
@Composable
private fun MainFloatingActionButtonsPreview() {
var destination by remember { mutableStateOf(MainNavigationListLocation.CHATS) }
private fun MainFloatingActionButtonsNavigationRailPreview() {
var currentDestination by remember { mutableStateOf(MainNavigationListLocation.CHATS) }
val callback = remember {
object : MainFloatingActionButtonsCallback {
override fun onCameraClick(destination: MainNavigationListLocation) {
currentDestination = MainNavigationListLocation.CALLS
}
override fun onNewChatClick() {
currentDestination = MainNavigationListLocation.STORIES
}
override fun onNewCallClick() {
currentDestination = MainNavigationListLocation.CHATS
}
}
}
Previews.Preview {
MainFloatingActionButtons(
destination = destination,
onCameraClick = { destination = MainNavigationListLocation.CALLS },
onNewChatClick = { destination = MainNavigationListLocation.STORIES },
onNewCallClick = { destination = MainNavigationListLocation.CHATS }
destination = currentDestination,
callback = callback,
navigation = Navigation.RAIL
)
}
}
@SignalPreview
@Composable
private fun MainFloatingActionButtonsNavigationBarPreview() {
var currentDestination by remember { mutableStateOf(MainNavigationListLocation.CHATS) }
val callback = remember {
object : MainFloatingActionButtonsCallback {
override fun onCameraClick(destination: MainNavigationListLocation) {
currentDestination = MainNavigationListLocation.CALLS
}
override fun onNewChatClick() {
currentDestination = MainNavigationListLocation.STORIES
}
override fun onNewCallClick() {
currentDestination = MainNavigationListLocation.CHATS
}
}
}
Previews.Preview {
MainFloatingActionButtons(
destination = currentDestination,
callback = callback,
navigation = Navigation.BAR
)
}
}

View File

@@ -19,9 +19,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
@@ -40,12 +37,10 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.IntSize
@@ -214,48 +209,17 @@ private fun Modifier.drawNavigationBarBadge(count: Int, compact: Boolean): Modif
@Composable
fun MainNavigationRail(
state: MainNavigationState,
mainFloatingActionButtonsCallback: MainFloatingActionButtonsCallback,
onDestinationSelected: (MainNavigationListLocation) -> Unit
) {
NavigationRail(
containerColor = SignalTheme.colors.colorSurface1,
header = {
FilledTonalIconButton(
onClick = { },
shape = RoundedCornerShape(18.dp),
modifier = Modifier
.padding(top = 56.dp, bottom = 16.dp)
.size(56.dp),
enabled = true,
colors = IconButtonDefaults.filledTonalIconButtonColors()
.copy(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onBackground
)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_edit_24),
contentDescription = null
)
}
FilledTonalIconButton(
onClick = { },
shape = RoundedCornerShape(18.dp),
modifier = Modifier
.padding(bottom = 80.dp)
.size(56.dp),
enabled = true,
colors = IconButtonDefaults.filledTonalIconButtonColors()
.copy(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_camera_24),
contentDescription = null
)
}
MainFloatingActionButtons(
destination = state.selectedDestination,
callback = mainFloatingActionButtonsCallback,
modifier = Modifier.padding(vertical = 40.dp)
)
}
) {
val entries = remember(state.isStoriesFeatureEnabled) {
@@ -351,7 +315,7 @@ private fun NavigationDestinationIcon(
LottieAnimation(
composition = composition,
progress = { progress },
progress = { if (selected) progress else 0f },
dynamicProperties = dynamicProperties,
modifier = Modifier.size(LOTTIE_SIZE)
)
@@ -383,6 +347,7 @@ private fun MainNavigationRailPreview() {
storiesCount = 5,
selectedDestination = selected
),
mainFloatingActionButtonsCallback = MainFloatingActionButtonsCallback.Empty,
onDestinationSelected = { selected = it }
)
}

View File

@@ -38,6 +38,7 @@ import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainFloatingActionButtonsCallback
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationState
@@ -45,7 +46,16 @@ import org.thoughtcrime.securesms.util.RemoteConfig
enum class Navigation {
RAIL,
BAR
BAR;
companion object {
@Composable
fun rememberNavigation(): Navigation {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
return remember(windowSizeClass) { windowSizeClass.navigation }
}
}
}
/**
@@ -275,6 +285,7 @@ private fun AppScaffoldPreview() {
navRailContent = {
MainNavigationRail(
state = MainNavigationState(),
mainFloatingActionButtonsCallback = MainFloatingActionButtonsCallback.Empty,
onDestinationSelected = {}
)
},