Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation Rail in Landscape Mode #284

Closed
wants to merge 12 commits into from
47 changes: 34 additions & 13 deletions app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.imashnake.animite.features

import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
Expand All @@ -8,11 +9,16 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
Expand All @@ -22,12 +28,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
import com.imashnake.animite.api.anilist.sanitize.media.MediaList
import com.imashnake.animite.core.extensions.orFalse
import com.imashnake.animite.core.ui.LocalPaddings
import com.imashnake.animite.features.home.HomeScreen
import com.imashnake.animite.features.searchbar.SearchFrontDrop
Expand All @@ -36,6 +44,7 @@ import com.imashnake.animite.media.MediaPage
import com.imashnake.animite.navigation.HomeRoute
import com.imashnake.animite.navigation.NavigationBar
import com.imashnake.animite.navigation.NavigationBarPaths
import com.imashnake.animite.navigation.NavigationRail
import com.imashnake.animite.navigation.ProfileRoute
import com.imashnake.animite.navigation.SocialRoute
import com.imashnake.animite.profile.ProfileScreen
Expand Down Expand Up @@ -67,17 +76,18 @@ class MainActivity : ComponentActivity() {
}
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MainScreen(modifier: Modifier = Modifier) {
val navController = rememberNavController()

val currentBackStackEntry by navController.currentBackStackEntryAsState()
val isNavBarVisible = remember(currentBackStackEntry) {
if (currentBackStackEntry != null) {
NavigationBarPaths.entries.any { it.matchesDestination(currentBackStackEntry!!) }
} else {
false
}
currentBackStackEntry?.let {
NavigationBarPaths.entries.any { path ->
path.matchesDestination(it)
}
}.orFalse()
}

// TODO: Refactor to use Scaffold once AnimatedVisibility issues are fixed;
Expand Down Expand Up @@ -120,7 +130,8 @@ fun MainScreen(modifier: Modifier = Modifier) {
}

SearchFrontDrop(
hasExtraPadding = isNavBarVisible,
hasExtraPadding = isNavBarVisible &&
(LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT),
onItemClick = { id, mediaType ->
navController.navigate(
MediaPage(
Expand All @@ -139,13 +150,23 @@ fun MainScreen(modifier: Modifier = Modifier) {
)
)

AnimatedVisibility(
visible = isNavBarVisible,
modifier = Modifier.align(Alignment.BottomCenter),
enter = slideInVertically { it },
exit = slideOutVertically { it }
) {
NavigationBar(navController = navController)
when(LocalConfiguration.current.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
AnimatedVisibility(
visible = isNavBarVisible && !WindowInsets.isImeVisible,
modifier = Modifier.align(Alignment.CenterStart),
enter = slideInHorizontally { -it },
exit = slideOutHorizontally { -it }
) { NavigationRail(navController = navController) }
}
else -> {
AnimatedVisibility(
visible = isNavBarVisible,
modifier = Modifier.align(Alignment.BottomCenter),
enter = slideInVertically { it },
exit = slideOutVertically { it }
) { NavigationBar(navController = navController) }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.imashnake.animite.features.home

import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.RuntimeShader
import android.os.Build
import androidx.compose.animation.AnimatedContent
Expand Down Expand Up @@ -53,6 +54,7 @@ import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -181,6 +183,9 @@ fun HomeScreen(
start = LocalPaddings.current.large,
bottom = LocalPaddings.current.medium
)
.thenIf(LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
padding(start = dimensionResource(navigationR.dimen.navigation_rail_width))
}
.landscapeCutoutPadding()
.weight(1f, fill = false),
maxLines = 1
Expand Down Expand Up @@ -240,8 +245,11 @@ fun HomeScreen(
},
contentModifier = Modifier.padding(
top = LocalPaddings.current.large / 2,
bottom = LocalPaddings.current.large / 2 +
dimensionResource(navigationR.dimen.navigation_bar_height)
bottom = LocalPaddings.current.large / 2,
).thenIf(
condition = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE,
other = { padding(start = dimensionResource(navigationR.dimen.navigation_rail_width)) },
elseOther = { padding(bottom = dimensionResource(navigationR.dimen.navigation_bar_height)) }
),
verticalArrangement = Arrangement.spacedBy(0.dp)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.imashnake.animite.core.extensions

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

@OptIn(ExperimentalContracts::class)
fun Boolean?.orFalse(): Boolean {
contract {
returns(true) implies (this@orFalse != null)
}
return this == true
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.heightIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.graphicsLayer
Expand All @@ -28,7 +29,9 @@ fun Modifier.maxHeight(max: Dp) = heightIn(0.dp, max)
/**
* [Adding modifiers conditionally in Jetpack Compose](https://patrickmichalik.com/blog/adding-modifiers-conditionally-in-jetpack-compose).
*/
@Composable
fun Modifier.thenIf(
condition: Boolean,
other: Modifier.() -> Modifier,
) = if (condition) other() else this
elseOther: @Composable Modifier.() -> Modifier = { this },
other: @Composable Modifier.() -> Modifier,
) = if (condition) other() else elseOther()
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ fun BannerLayout(
.fillMaxSize()
.padding(top = bannerHeight)
.background(MaterialTheme.colorScheme.background)
.navigationBarsPadding()
.then(contentModifier),
verticalArrangement = verticalArrangement
) { content() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,38 @@
package com.imashnake.animite.navigation

import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState

@Composable
fun NavigationBar(
navController: NavController
navController: NavController,
modifier: Modifier = Modifier,
) {
val currentBackStackEntry by navController.currentBackStackEntryAsState()

// TODO: Can we use `navigationBarsPadding()` instead?
NavigationBar(
Modifier.height(
modifier = modifier.height(
dimensionResource(R.dimen.navigation_bar_height) + WindowInsets
.navigationBars
.asPaddingValues()
.calculateBottomPadding()
),
// TODO: Use a `NavigationRail` instead.
// TODO: Remove this after adding the rail.
windowInsets = if (LocalConfiguration.current.orientation
== Configuration.ORIENTATION_LANDSCAPE
) { WindowInsets.displayCutout } else { WindowInsets(0.dp) }
Expand All @@ -52,74 +42,11 @@ fun NavigationBar(
currentBackStackEntry?.let { destination.matchesDestination(it) } ?: false
}
NavigationBarItem(
modifier = Modifier.navigationBarsPadding(),
selected = selected,
onClick = {
if (!selected) destination.navigateTo(navController)
},
icon = destination.icon
onClick = { if (!selected) destination.navigateTo(navController) },
icon = destination.icon,
modifier = Modifier.navigationBarsPadding(),
)
}
}
}

enum class NavigationBarPaths(
val navigateTo: (NavController) -> Unit,
val matchesDestination: (NavBackStackEntry) -> Boolean,
val icon: @Composable () -> Unit,
@StringRes val labelRes: Int
) {
Social(
navigateTo = {
it.navigate(SocialRoute) {
popUpTo(id = it.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
}
},
matchesDestination = {
it.destination.hierarchy.any { it.hasRoute(SocialRoute::class) }
},
icon = {
Icon(ImageVector.vectorResource(R.drawable.social), contentDescription = stringResource(R.string.social))
},
labelRes = R.string.social
),
Home(
navigateTo = {
it.navigate(HomeRoute) {
popUpTo(id = it.graph.findStartDestination().id) {
saveState = true
inclusive = true
}
launchSingleTop = true
}
},
matchesDestination = {
it.destination.hierarchy.any { it.hasRoute(HomeRoute::class) }
},
icon = {
Icon(ImageVector.vectorResource(R.drawable.home), contentDescription = stringResource(R.string.home))
},
labelRes = R.string.home
),

Profile(
navigateTo = {
it.navigate(ProfileRoute()) {
popUpTo(id = it.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
}
},
matchesDestination = {
it.destination.hierarchy.any { it.hasRoute(ProfileRoute::class) }
},
icon = {
Icon(ImageVector.vectorResource(R.drawable.profile), contentDescription = stringResource(R.string.profile))
},
labelRes = R.string.profile
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.imashnake.animite.navigation

import androidx.annotation.StringRes
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination

enum class NavigationBarPaths(
val navigateTo: (NavController) -> Unit,
val matchesDestination: (NavBackStackEntry) -> Boolean,
val icon: @Composable () -> Unit,
@StringRes val labelRes: Int
) {
Social(
navigateTo = {
it.navigate(SocialRoute) {
popUpTo(id = it.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
}
},
matchesDestination = {
it.destination.hierarchy.any { it.hasRoute(SocialRoute::class) }
},
icon = {
Icon(ImageVector.vectorResource(R.drawable.social), contentDescription = stringResource(R.string.social))
},
labelRes = R.string.social
),
Home(
navigateTo = {
it.navigate(HomeRoute) {
popUpTo(id = it.graph.findStartDestination().id) {
saveState = true
inclusive = true
}
launchSingleTop = true
}
},
matchesDestination = {
it.destination.hierarchy.any { it.hasRoute(HomeRoute::class) }
},
icon = {
Icon(ImageVector.vectorResource(R.drawable.home), contentDescription = stringResource(R.string.home))
},
labelRes = R.string.home
),

Profile(
navigateTo = {
it.navigate(ProfileRoute()) {
popUpTo(id = it.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
}
},
matchesDestination = {
it.destination.hierarchy.any { it.hasRoute(ProfileRoute::class) }
},
icon = {
Icon(ImageVector.vectorResource(R.drawable.profile), contentDescription = stringResource(R.string.profile))
},
labelRes = R.string.profile
),
}
Loading