mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 13:39:24 +00:00
Wire up nav rail fabs and fix animation playing on leaving a tab.
This commit is contained in:
committed by
Cody Henthorne
parent
8b7b184224
commit
c5e795b176
@@ -268,6 +268,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
if (isNavigationVisible) {
|
||||
MainNavigationRail(
|
||||
state = mainNavigationState,
|
||||
mainFloatingActionButtonsCallback = mainBottomChromeCallback,
|
||||
onDestinationSelected = mainNavigationCallback
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user