mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 22:17:03 +05:30
feat: redesign bottom navigation bar
This commit is contained in:
parent
4f3bafc051
commit
1de4916c9c
14 changed files with 211 additions and 64 deletions
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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") },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue