feat: redesign bottom navigation bar

This commit is contained in:
Harsh Shandilya 2024-08-28 15:16:42 +05:30
parent 4f3bafc051
commit 1de4916c9c
14 changed files with 211 additions and 64 deletions

View file

@ -95,6 +95,8 @@ dependencies {
implementation(libs.copydown)
implementation(libs.dagger)
implementation(libs.eithernet)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(libs.javax.inject)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core)

View file

@ -13,31 +13,37 @@ import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarScrollBehavior
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.hazeChild
import dev.msfjarvis.claw.android.ui.navigation.Destination
import dev.msfjarvis.claw.android.ui.navigation.matches
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
import kotlinx.collections.immutable.ImmutableList
const val AnimationDuration = 100
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ClawNavigationBar(
navController: NavController,
items: ImmutableList<NavigationItem>,
isVisible: Boolean,
scrollBehavior: BottomAppBarScrollBehavior,
hazeState: HazeState,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
@ -54,40 +60,60 @@ fun ClawNavigationBar(
targetOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
),
modifier = Modifier,
) {
BottomAppBar(modifier = modifier, scrollBehavior = scrollBehavior) {
val navBackStackEntry = navController.currentBackStackEntryAsState().value
val currentDestination = navBackStackEntry?.destination
items.forEach { navItem ->
val isSelected = currentDestination.matches(navItem.destination)
NavigationBarItem(
icon = {
Crossfade(isSelected, label = "nav-label") {
Icon(
imageVector = if (it) navItem.selectedIcon else navItem.icon,
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
)
}
},
label = { Text(text = navItem.label) },
selected = isSelected,
onClick = {
if (isSelected) {
navItem.listStateResetCallback()
} else {
navController.navigate(navItem.destination) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
label = "",
content = {
FloatingNavigationBar(
tonalElevation = 16.dp,
shape = MaterialTheme.shapes.extraLarge,
modifier =
modifier
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.clip(MaterialTheme.shapes.extraLarge)
.hazeChild(
hazeState,
style =
HazeStyle(
backgroundColor = MaterialTheme.colorScheme.surface,
tints = emptyList(),
blurRadius = 24.dp,
noiseFactor = 0f,
),
),
containerColor = Color.Transparent,
) {
val navBackStackEntry = navController.currentBackStackEntryAsState().value
val currentDestination = navBackStackEntry?.destination
items.forEach { navItem ->
val isSelected = currentDestination.matches(navItem.destination)
NavigationBarItem(
icon = {
Crossfade(isSelected, label = "nav-label") {
Icon(
imageVector = if (it) navItem.selectedIcon else navItem.icon,
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
)
}
}
},
modifier = Modifier.testTag(navItem.label.uppercase()),
)
},
label = { Text(text = navItem.label) },
selected = isSelected,
onClick = {
if (isSelected) {
navItem.listStateResetCallback()
} else {
navController.navigate(navItem.destination) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
},
modifier = Modifier.testTag(navItem.label.uppercase()),
)
}
}
}
}
},
)
}
class NavigationItem(

View file

@ -10,6 +10,7 @@ import androidx.activity.compose.ReportDrawn
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -36,6 +37,7 @@ fun DatabasePosts(
items: ImmutableMap<String, List<UIPost>>,
listState: LazyListState,
postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
ReportDrawn()
@ -50,7 +52,7 @@ fun DatabasePosts(
Text(text = "No saved posts", style = MaterialTheme.typography.headlineSmall)
}
} else {
LazyColumn(state = listState) {
LazyColumn(state = listState, contentPadding = contentPadding) {
items.forEach { (month, posts) ->
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->

View file

@ -7,6 +7,7 @@
package dev.msfjarvis.claw.android.ui.lists
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -47,6 +48,7 @@ fun NetworkPosts(
lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState,
postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
@ -72,7 +74,7 @@ fun NetworkPosts(
modifier = Modifier.align(Alignment.Center),
)
} else {
LazyColumn(state = listState) {
LazyColumn(contentPadding = contentPadding, state = listState) {
items(
count = lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.shortId },
@ -109,6 +111,7 @@ private fun ListPreview() {
lazyPagingItems = flow.collectAsLazyPagingItems(),
listState = rememberLazyListState(),
postActions = TEST_POST_ACTIONS,
contentPadding = PaddingValues(),
)
}
}

View file

@ -7,6 +7,7 @@
package dev.msfjarvis.claw.android.ui.lists
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
@ -32,6 +33,7 @@ fun SearchList(
searchQuery: String,
setSearchQuery: (String) -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
val lazyPagingItems = items.collectAsLazyPagingItems()
val triggerSearch = { query: String ->
@ -49,6 +51,7 @@ fun SearchList(
lazyPagingItems = lazyPagingItems,
listState = listState,
postActions = postActions,
contentPadding = contentPadding,
)
}
}

View file

@ -13,7 +13,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
@ -26,7 +25,6 @@ import androidx.compose.material.icons.filled.Whatshot
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Whatshot
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -45,7 +43,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -61,6 +58,8 @@ import androidx.navigation.toRoute
import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.msfjarvis.claw.android.MainActivity
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.SearchActivity
@ -111,8 +110,7 @@ fun LobstersPostsScreen(
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val postActions = rememberPostActions(context, urlLauncher, navController, viewModel)
val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
val hazeState = remember { HazeState() }
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
@ -209,17 +207,14 @@ fun LobstersPostsScreen(
navController = navController,
items = navItems,
isVisible = currentDestination.any(navDestinations),
scrollBehavior = scrollBehavior,
hazeState = hazeState,
)
}
},
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier =
modifier.nestedScroll(scrollBehavior.nestedScrollConnection).semantics {
testTagsAsResourceId = true
},
) { paddingValues ->
Row(modifier = Modifier.padding(paddingValues)) {
modifier = modifier.semantics { testTagsAsResourceId = true },
) { contentPadding ->
Row {
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
ClawNavigationRail(
navController = navController,
@ -234,6 +229,7 @@ fun LobstersPostsScreen(
// Make animations 2x faster than default specs
enterTransition = { fadeIn(animationSpec = tween(350)) },
exitTransition = { fadeOut(animationSpec = tween(350)) },
modifier = Modifier.haze(hazeState),
) {
composable<Hottest> {
setWebUri("https://lobste.rs/")
@ -241,6 +237,7 @@ fun LobstersPostsScreen(
lazyPagingItems = hottestPosts,
listState = hottestListState,
postActions = postActions,
contentPadding = contentPadding,
)
}
composable<Newest> {
@ -249,11 +246,17 @@ fun LobstersPostsScreen(
lazyPagingItems = newestPosts,
listState = newestListState,
postActions = postActions,
contentPadding = contentPadding,
)
}
composable<Saved> {
setWebUri(null)
DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions)
DatabasePosts(
items = savedPosts,
listState = savedListState,
postActions = postActions,
contentPadding = contentPadding,
)
}
composable<Comments> { backStackEntry ->
val postId = backStackEntry.toRoute<Comments>().postId
@ -264,6 +267,7 @@ fun LobstersPostsScreen(
htmlConverter = htmlConverter,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
contentPadding = contentPadding,
openUserProfile = { navController.navigate(User(it)) },
)
}
@ -273,6 +277,7 @@ fun LobstersPostsScreen(
UserProfile(
username = username,
getProfile = viewModel::getUserProfile,
contentPadding = contentPadding,
openUserProfile = { navController.navigate(User(it)) },
)
}
@ -284,9 +289,12 @@ fun LobstersPostsScreen(
exportPostsAsJson = viewModel::exportPostsAsJson,
exportPostsAsHtml = viewModel::exportPostsAsHtml,
snackbarHostState = snackbarHostState,
contentPadding = contentPadding,
)
}
composable<AboutLibraries> { LibrariesContainer(modifier = Modifier.fillMaxSize()) }
composable<AboutLibraries> {
LibrariesContainer(contentPadding = contentPadding, modifier = Modifier.fillMaxSize())
}
}
}
}

View file

@ -6,7 +6,6 @@
*/
package dev.msfjarvis.claw.android.ui.screens
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
@ -39,12 +38,8 @@ fun SearchScreen(
val navController = rememberNavController()
val postActions = rememberPostActions(LocalContext.current, urlLauncher, navController, viewModel)
val listState = rememberLazyListState()
Scaffold(modifier = modifier) { paddingValues ->
NavHost(
navController = navController,
startDestination = Search,
modifier = Modifier.padding(paddingValues),
) {
Scaffold(modifier = modifier) { contentPadding ->
NavHost(navController = navController, startDestination = Search) {
composable<Search> {
setWebUri("https://lobste.rs/search")
SearchList(
@ -52,6 +47,7 @@ fun SearchScreen(
listState = listState,
postActions = postActions,
searchQuery = viewModel.searchQuery,
contentPadding = contentPadding,
setSearchQuery = { query -> viewModel.searchQuery = query },
)
}
@ -65,6 +61,7 @@ fun SearchScreen(
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
openUserProfile = { navController.navigate(User(it)) },
contentPadding = contentPadding,
)
}
composable<User> { backStackEntry ->
@ -74,6 +71,7 @@ fun SearchScreen(
username = username,
getProfile = viewModel::getUserProfile,
openUserProfile = { navController.navigate(User(it)) },
contentPadding = contentPadding,
)
}
}

View file

@ -13,11 +13,13 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.filled.Bookmarks
@ -55,10 +57,11 @@ fun SettingsScreen(
importPosts: suspend (InputStream) -> Unit,
exportPostsAsJson: suspend (OutputStream) -> Unit,
exportPostsAsHtml: suspend (OutputStream) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
Box(modifier = modifier.fillMaxSize()) {
Box(modifier = modifier.padding(contentPadding).fillMaxSize()) {
Column {
ListItem(
headlineContent = { Text("Data transfer") },