add hamburger icon to rail

This commit is contained in:
2026-08-16 10:37:01 +03:00
parent d2adcc8963
commit b23307e6dd
8 changed files with 232 additions and 175 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
alias(libs.plugins.convention.ktor.client)
}
val appVersion = "1.26.4"
val appVersion = "1.26.5"
android {
buildFeatures { buildConfig = true }
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#fff"
android:pathData="M120,720L120,640L840,640L840,720L120,720ZM120,520L120,440L840,440L840,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z" />
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#fff"
android:pathData="M120,720L120,640L640,640L640,720L120,720ZM784,680L584,480L784,280L840,336L696,480L840,624L784,680ZM120,520L120,440L520,440L520,520L120,520ZM120,320L120,240L640,240L640,320L120,320Z" />
</vector>
@@ -1,52 +1,22 @@
package ru.shadowsparky.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.WideNavigationRail
import androidx.compose.material3.WideNavigationRailItem
import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.nav.Route
@Immutable
@@ -141,128 +111,3 @@ fun <T : Route> AppScaffold(
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun <T : Route> DynamicNavigationScaffold(
modifier: Modifier,
navigationSuiteType: NavigationSuiteType,
navigationEntries: List<NavigationEntry<T>>?,
currentRoute: T?,
onSelectNavigation: ((T) -> Unit)?,
scrollBehavior: TopAppBarScrollBehavior?,
appBar: @Composable (TopAppBarScrollBehavior?) -> Unit,
snackbarState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit,
) {
val entries = navigationEntries.orEmpty()
val railState = rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
val isExpanded = railState.targetValue == WideNavigationRailValue.Expanded
var railHasFocus by remember { mutableStateOf(false) }
LaunchedEffect(railHasFocus) {
if (railHasFocus) railState.expand() else railState.collapse()
}
NavigationSuiteScaffoldLayout(
navigationSuiteType = navigationSuiteType,
navigationSuite = {
WideNavigationRail(
state = railState,
modifier = Modifier.onFocusChanged { railHasFocus = it.hasFocus }.focusGroup()
) {
entries.forEach { entry ->
WideNavigationRailItem(
railExpanded = isExpanded,
selected = currentRoute == entry.route,
onClick = { onSelectNavigation?.invoke(entry.route) },
icon = { Icon(entry.painter, entry.title) },
label = {
if (isExpanded) Text(entry.title)
}
)
}
}
}
) {
Scaffold(
modifier = if (scrollBehavior != null) {
modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
modifier
},
topBar = { appBar(scrollBehavior) },
snackbarHost = {
snackbarState?.let { SnackbarHost(it) }
},
content = content
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun <T : Route> BottomBarScaffold(
modifier: Modifier,
navigationEntries: List<NavigationEntry<T>>?,
currentRoute: T?,
appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
content: @Composable (padding: PaddingValues) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
onSelectNavigation: ((T) -> Unit)?,
snackbarState: SnackbarHostState?
) {
Scaffold(
bottomBar = { BottomBar(navigationEntries, currentRoute, onSelectNavigation) },
topBar = { appBar(scrollBehavior) },
modifier = if (scrollBehavior != null) {
modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
modifier
}, snackbarHost = {
snackbarState?.let { SnackbarHost(it) }
},
content = content
)
}
@Composable
private fun <T : Route> BottomBar(
navigationEntries: List<NavigationEntry<T>>?,
currentRoute: T?,
onSelectNavigation: ((T) -> Unit)?
) {
navigationEntries?.let { entries ->
Box(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
listOf(Color.Transparent, MaterialTheme.colorScheme.surfaceContainer)
)
)
.padding(8.dp)
.windowInsetsPadding(WindowInsets.navigationBars)
) {
NavigationBar(
containerColor = navBarContentColor(),
modifier = Modifier.clip(RoundedCornerShape(24.dp)).blurEffect()
) {
entries.forEach { entry ->
NavigationBarItem(
selected = currentRoute == entry.route,
onClick = { onSelectNavigation?.invoke(entry.route) },
icon = { Icon(entry.painter, contentDescription = entry.title) }
)
}
}
}
}
}
@Composable
private fun navBarContentColor(): Color {
return if (isBlurEnabled()) {
MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
} else {
NavigationBarDefaults.containerColor
}
}
@@ -0,0 +1,98 @@
package ru.shadowsparky.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.nav.Route
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun <T : Route> BottomBarScaffold(
modifier: Modifier,
navigationEntries: List<NavigationEntry<T>>?,
currentRoute: T?,
appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
content: @Composable (padding: PaddingValues) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
onSelectNavigation: ((T) -> Unit)?,
snackbarState: SnackbarHostState?
) {
Scaffold(
bottomBar = { BottomBar(navigationEntries, currentRoute, onSelectNavigation) },
topBar = { appBar(scrollBehavior) },
modifier = if (scrollBehavior != null) {
modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
modifier
}, snackbarHost = {
snackbarState?.let { SnackbarHost(it) }
},
content = content
)
}
@Composable
private fun <T : Route> BottomBar(
navigationEntries: List<NavigationEntry<T>>?,
currentRoute: T?,
onSelectNavigation: ((T) -> Unit)?
) {
navigationEntries?.let { entries ->
Box(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
listOf(Color.Transparent, MaterialTheme.colorScheme.surfaceContainer)
)
)
.padding(8.dp)
.windowInsetsPadding(WindowInsets.navigationBars)
) {
NavigationBar(
containerColor = navBarContentColor(),
modifier = Modifier.clip(RoundedCornerShape(24.dp)).blurEffect()
) {
entries.forEach { entry ->
NavigationBarItem(
selected = currentRoute == entry.route,
onClick = { onSelectNavigation?.invoke(entry.route) },
icon = { Icon(entry.painter, contentDescription = entry.title) }
)
}
}
}
}
}
@Composable
private fun navBarContentColor(): Color {
return if (isBlurEnabled()) {
MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
} else {
NavigationBarDefaults.containerColor
}
}
@@ -0,0 +1,93 @@
package ru.shadowsparky.ui
import androiddev.libs.ui.generated.resources.Res
import androiddev.libs.ui.generated.resources.menu
import androiddev.libs.ui.generated.resources.menu_open
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.WideNavigationRail
import androidx.compose.material3.WideNavigationRailItem
import androidx.compose.material3.WideNavigationRailState
import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import org.jetbrains.compose.resources.painterResource
import ru.shadowsparky.ui.nav.Route
internal val LocalWideNavigationRailState = compositionLocalOf<WideNavigationRailState?> { null }
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
internal fun <T : Route> DynamicNavigationScaffold(
modifier: Modifier,
navigationSuiteType: NavigationSuiteType,
navigationEntries: List<NavigationEntry<T>>?,
currentRoute: T?,
onSelectNavigation: ((T) -> Unit)?,
scrollBehavior: TopAppBarScrollBehavior?,
appBar: @Composable (TopAppBarScrollBehavior?) -> Unit,
snackbarState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit,
) {
val entries = navigationEntries.orEmpty()
val railState = LocalWideNavigationRailState.current
?: rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
var isExpanded by remember {
mutableStateOf(railState.targetValue == WideNavigationRailValue.Expanded)
}
LaunchedEffect(isExpanded) {
if (!isExpanded) railState.collapse() else railState.expand()
}
NavigationSuiteScaffoldLayout(
navigationSuiteType = navigationSuiteType,
navigationSuite = {
WideNavigationRail(state = railState) {
val resId = if (isExpanded) Res.drawable.menu_open else Res.drawable.menu
WideNavigationRailItem(
false,
onClick = { isExpanded = !isExpanded },
icon = { Icon(painterResource(resId), null) },
label = {},
railExpanded = isExpanded
)
entries.forEach { entry ->
WideNavigationRailItem(
railExpanded = isExpanded,
selected = currentRoute == entry.route,
onClick = { onSelectNavigation?.invoke(entry.route) },
icon = { Icon(entry.painter, entry.title) },
label = { if (isExpanded) Text(entry.title) }
)
}
}
}
) {
Scaffold(
modifier = if (scrollBehavior != null) {
modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
modifier
},
topBar = { appBar(scrollBehavior) },
snackbarHost = { snackbarState?.let { SnackbarHost(it) } },
content = content
)
}
}
@@ -104,21 +104,14 @@ fun ExpressiveLazyColumn(
verticalArrangement = verticalArrangement
) {
scope.sections.forEachIndexed { sectionIndex, section ->
if (sectionIndex != 0 && sectionSpacing > 0.dp) {
item(key = "sectionSpacer_$sectionIndex") {
Spacer(
Modifier
.animateItem()
.height(sectionSpacing)
)
Spacer(Modifier.animateItem().height(sectionSpacing))
}
}
section.nodes.forEachIndexed { index, node ->
val topRadius = if (index == 0) roundedMax else roundedMin
val bottomRadius = if (index == section.nodes.lastIndex) roundedMax else roundedMin
item(key = node.key, contentType = node.contentType) {
ExpressiveCard(
topRadius = if (reverseLayout) bottomRadius else topRadius,
@@ -2,13 +2,17 @@ package ru.shadowsparky.ui.nav
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatableV2
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import ru.shadowsparky.ui.LocalWideNavigationRailState
import ru.shadowsparky.ui.theme.getColorScheme
@OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -17,6 +21,8 @@ fun Nav(
rootComponent: RootComponent<*>,
process: @Composable (child: Child, route: Route) -> Unit
) {
val railState = rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
CompositionLocalProvider(LocalWideNavigationRailState provides railState) {
MaterialExpressiveTheme(getColorScheme()) {
Children(
stack = rootComponent.valueChildStack,
@@ -30,4 +36,5 @@ fun Nav(
process(it.instance, it.configuration as Route)
}
}
}
}