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) alias(libs.plugins.convention.ktor.client)
} }
val appVersion = "1.26.4" val appVersion = "1.26.5"
android { android {
buildFeatures { buildConfig = true } 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 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.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.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 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.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior 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.currentWindowAdaptiveInfoV2
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults 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.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable 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.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.graphics.painter.Painter
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.nav.Route import ru.shadowsparky.ui.nav.Route
@Immutable @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 verticalArrangement = verticalArrangement
) { ) {
scope.sections.forEachIndexed { sectionIndex, section -> scope.sections.forEachIndexed { sectionIndex, section ->
if (sectionIndex != 0 && sectionSpacing > 0.dp) { if (sectionIndex != 0 && sectionSpacing > 0.dp) {
item(key = "sectionSpacer_$sectionIndex") { item(key = "sectionSpacer_$sectionIndex") {
Spacer( Spacer(Modifier.animateItem().height(sectionSpacing))
Modifier
.animateItem()
.height(sectionSpacing)
)
} }
} }
section.nodes.forEachIndexed { index, node -> section.nodes.forEachIndexed { index, node ->
val topRadius = if (index == 0) roundedMax else roundedMin val topRadius = if (index == 0) roundedMax else roundedMin
val bottomRadius = if (index == section.nodes.lastIndex) roundedMax else roundedMin val bottomRadius = if (index == section.nodes.lastIndex) roundedMax else roundedMin
item(key = node.key, contentType = node.contentType) { item(key = node.key, contentType = node.contentType) {
ExpressiveCard( ExpressiveCard(
topRadius = if (reverseLayout) bottomRadius else topRadius, topRadius = if (reverseLayout) bottomRadius else topRadius,
@@ -2,13 +2,17 @@ package ru.shadowsparky.ui.nav
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children 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.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatableV2 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.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import ru.shadowsparky.ui.LocalWideNavigationRailState
import ru.shadowsparky.ui.theme.getColorScheme import ru.shadowsparky.ui.theme.getColorScheme
@OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -17,17 +21,20 @@ fun Nav(
rootComponent: RootComponent<*>, rootComponent: RootComponent<*>,
process: @Composable (child: Child, route: Route) -> Unit process: @Composable (child: Child, route: Route) -> Unit
) { ) {
MaterialExpressiveTheme(getColorScheme()) { val railState = rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
Children( CompositionLocalProvider(LocalWideNavigationRailState provides railState) {
stack = rootComponent.valueChildStack, MaterialExpressiveTheme(getColorScheme()) {
animation = predictiveBackAnimation( Children(
backHandler = rootComponent.backHandler, stack = rootComponent.valueChildStack,
fallbackAnimation = stackAnimation(fade()), animation = predictiveBackAnimation(
onBack = rootComponent::onBack, backHandler = rootComponent.backHandler,
selector = { backEvent, _, _ -> androidPredictiveBackAnimatableV2(backEvent) }, fallbackAnimation = stackAnimation(fade()),
), onBack = rootComponent::onBack,
) { selector = { backEvent, _, _ -> androidPredictiveBackAnimatableV2(backEvent) },
process(it.instance, it.configuration as Route) ),
) {
process(it.instance, it.configuration as Route)
}
} }
} }
} }