mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 22:17:03 +05:30
feat: add adaptive navigation for different screen sizes
This commit is contained in:
parent
b2470ca89b
commit
78b77fd4c1
8 changed files with 209 additions and 72 deletions
|
@ -53,6 +53,7 @@ dependencies {
|
||||||
implementation(libs.androidx.compose.material)
|
implementation(libs.androidx.compose.material)
|
||||||
implementation(libs.androidx.compose.material.icons.extended)
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.material3.window.size)
|
||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.androidx.lifecycle.compose)
|
implementation(libs.androidx.lifecycle.compose)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.deliveryhero.whetstone.Whetstone
|
import com.deliveryhero.whetstone.Whetstone
|
||||||
|
@ -27,15 +29,19 @@ class MainActivity : ComponentActivity() {
|
||||||
@Inject lateinit var htmlConverter: HTMLConverter
|
@Inject lateinit var htmlConverter: HTMLConverter
|
||||||
private var webUri: String? = null
|
private var webUri: String? = null
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
Whetstone.inject(this)
|
Whetstone.inject(this)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
setContent {
|
setContent {
|
||||||
|
val windowSizeClass = calculateWindowSizeClass(this)
|
||||||
|
|
||||||
LobstersApp(
|
LobstersApp(
|
||||||
urlLauncher = urlLauncher,
|
urlLauncher = urlLauncher,
|
||||||
htmlConverter = htmlConverter,
|
htmlConverter = htmlConverter,
|
||||||
|
windowSizeClass = windowSizeClass,
|
||||||
setWebUri = { url -> webUri = url },
|
setWebUri = { url -> webUri = url },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
*/
|
*/
|
||||||
package dev.msfjarvis.claw.android.ui
|
package dev.msfjarvis.claw.android.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -20,6 +22,7 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -44,10 +47,12 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||||
import dev.msfjarvis.claw.android.R
|
import dev.msfjarvis.claw.android.R
|
||||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||||
|
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||||
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
|
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
|
||||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||||
|
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||||
import dev.msfjarvis.claw.api.LobstersApi
|
import dev.msfjarvis.claw.api.LobstersApi
|
||||||
|
@ -66,6 +71,7 @@ import kotlinx.coroutines.launch
|
||||||
fun LobstersApp(
|
fun LobstersApp(
|
||||||
urlLauncher: UrlLauncher,
|
urlLauncher: UrlLauncher,
|
||||||
htmlConverter: HTMLConverter,
|
htmlConverter: HTMLConverter,
|
||||||
|
windowSizeClass: WindowSizeClass,
|
||||||
setWebUri: (String?) -> Unit,
|
setWebUri: (String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: ClawViewModel = injectedViewModel(),
|
viewModel: ClawViewModel = injectedViewModel(),
|
||||||
|
@ -84,6 +90,8 @@ fun LobstersApp(
|
||||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||||
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
||||||
|
|
||||||
|
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||||
|
|
||||||
LobstersTheme(
|
LobstersTheme(
|
||||||
dynamicColor = true,
|
dynamicColor = true,
|
||||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||||
|
@ -141,18 +149,28 @@ fun LobstersApp(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||||
ClawNavigationBar(
|
ClawNavigationBar(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
items = navItems,
|
items = navItems,
|
||||||
isVisible = navItems.any { it.route == currentDestination },
|
isVisible = navItems.any { it.route == currentDestination },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
Row(modifier = Modifier.padding(paddingValues)) {
|
||||||
|
AnimatedVisibility(visible = navigationType != ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||||
|
ClawNavigationRail(
|
||||||
|
navController = navController,
|
||||||
|
items = navItems,
|
||||||
|
isVisible = navItems.any { it.route == currentDestination },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Destinations.startDestination.route,
|
startDestination = Destinations.startDestination.route,
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
) {
|
) {
|
||||||
val uri = LobstersApi.BASE_URL
|
val uri = LobstersApi.BASE_URL
|
||||||
composable(
|
composable(
|
||||||
|
@ -223,3 +241,4 @@ fun LobstersApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import androidx.navigation.NavController
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
private const val AnimationDuration = 100
|
const val AnimationDuration = 100
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClawNavigationBar(
|
fun ClawNavigationBar(
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2022-2023 Harsh Shandilya.
|
||||||
|
* Use of this source code is governed by an MIT-style
|
||||||
|
* license that can be found in the LICENSE file or at
|
||||||
|
* https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
package dev.msfjarvis.claw.android.ui.decorations
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
|
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.foundation.layout.Spacer
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.NavigationRail
|
||||||
|
import androidx.compose.material3.NavigationRailItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClawNavigationRail(
|
||||||
|
navController: NavController,
|
||||||
|
items: ImmutableList<NavigationItem>,
|
||||||
|
isVisible: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isVisible,
|
||||||
|
enter =
|
||||||
|
slideInVertically(
|
||||||
|
// Enters by sliding up from offset 0 to fullHeight.
|
||||||
|
initialOffsetY = { fullHeight -> fullHeight },
|
||||||
|
animationSpec = tween(durationMillis = AnimationDuration, easing = LinearOutSlowInEasing),
|
||||||
|
),
|
||||||
|
exit =
|
||||||
|
slideOutVertically(
|
||||||
|
// Exits by sliding up from offset 0 to -fullHeight.
|
||||||
|
targetOffsetY = { fullHeight -> fullHeight },
|
||||||
|
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
|
||||||
|
),
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
NavigationRail(modifier = modifier) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
items.forEach { navItem ->
|
||||||
|
val isCurrentDestination = navController.currentDestination?.route == navItem.route
|
||||||
|
NavigationRailItem(
|
||||||
|
icon = {
|
||||||
|
Crossfade(isCurrentDestination, label = "nav-label") {
|
||||||
|
Icon(
|
||||||
|
painter = if (it) navItem.selectedIcon else navItem.icon,
|
||||||
|
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(text = navItem.label) },
|
||||||
|
selected = isCurrentDestination,
|
||||||
|
onClick = {
|
||||||
|
if (isCurrentDestination) {
|
||||||
|
navItem.listStateResetCallback()
|
||||||
|
return@NavigationRailItem
|
||||||
|
}
|
||||||
|
navController.graph.startDestinationRoute?.let { startDestination ->
|
||||||
|
navController.popBackStack(startDestination, false)
|
||||||
|
}
|
||||||
|
if (navItem.route != Destinations.startDestination.route) {
|
||||||
|
navController.navigate(navItem.route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag(navItem.label.uppercase()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2023 Harsh Shandilya.
|
||||||
|
* Use of this source code is governed by an MIT-style
|
||||||
|
* license that can be found in the LICENSE file or at
|
||||||
|
* https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
package dev.msfjarvis.claw.android.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||||
|
|
||||||
|
enum class ClawNavigationType {
|
||||||
|
BOTTOM_NAVIGATION,
|
||||||
|
NAVIGATION_RAIL;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromSize(windowSize: WindowWidthSizeClass): ClawNavigationType {
|
||||||
|
return when (windowSize) {
|
||||||
|
WindowWidthSizeClass.Compact -> BOTTOM_NAVIGATION
|
||||||
|
WindowWidthSizeClass.Medium,
|
||||||
|
WindowWidthSizeClass.Expanded -> NAVIGATION_RAIL
|
||||||
|
else -> BOTTOM_NAVIGATION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation
|
||||||
androidx-compose-material = { module = "androidx.compose.material:material" }
|
androidx-compose-material = { module = "androidx.compose.material:material" }
|
||||||
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
||||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||||
|
androidx-compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" }
|
||||||
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||||
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||||
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
||||||
|
|
|
@ -109,6 +109,7 @@ dependencyResolutionManagement {
|
||||||
includeGroup("androidx.vectordrawable")
|
includeGroup("androidx.vectordrawable")
|
||||||
includeGroup("androidx.versionedparcelable")
|
includeGroup("androidx.versionedparcelable")
|
||||||
includeGroup("androidx.viewpager")
|
includeGroup("androidx.viewpager")
|
||||||
|
includeGroup("androidx.window")
|
||||||
includeGroup("androidx.work")
|
includeGroup("androidx.work")
|
||||||
includeGroup("com.android")
|
includeGroup("com.android")
|
||||||
includeGroup("com.android.tools")
|
includeGroup("com.android.tools")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue