feat: add adaptive navigation for different screen sizes

This commit is contained in:
Yash-Garg 2023-05-28 13:08:26 +05:30
parent b2470ca89b
commit 78b77fd4c1
No known key found for this signature in database
8 changed files with 209 additions and 72 deletions

View file

@ -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)

View file

@ -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 },
) )
} }

View file

@ -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(
@ -222,4 +240,5 @@ fun LobstersApp(
} }
} }
} }
}
} }

View file

@ -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(

View file

@ -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))
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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" }

View file

@ -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")