update scaffolds

This commit is contained in:
2026-08-15 12:25:09 +03:00
parent 236ebb1462
commit 4f7d3dc6bc
26 changed files with 201 additions and 201 deletions
+2
View File
@@ -13,6 +13,8 @@ kotlin {
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(libs.ui.tooling.preview)
implementation(libs.adaptive.layout)
implementation(libs.material3.adaptive.navigation.suite)
}
}
}
@@ -3,14 +3,12 @@ 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.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.fillMaxSize
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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -19,13 +17,17 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
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.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItem
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
@@ -35,9 +37,9 @@ 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.LayoutDirection
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.nav.Route
import ru.shadowsparky.ui.theme.focusBorder
@Immutable
data class NavigationEntry<out T : Route>(
@@ -64,11 +66,15 @@ fun AppScaffold(
)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalMaterial3ExpressiveApi::class,
)
@Composable
fun <T : Route> AppScaffold(
modifier: Modifier = Modifier,
scrollBehavior: TopAppBarScrollBehavior? = TopAppBarDefaults.pinnedScrollBehavior(),
scrollBehavior: TopAppBarScrollBehavior? =
TopAppBarDefaults.pinnedScrollBehavior(),
snackbarState: SnackbarHostState? = null,
currentRoute: T? = null,
navigationEntries: List<NavigationEntry<T>>? = null,
@@ -76,40 +82,111 @@ fun <T : Route> AppScaffold(
appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
content: @Composable (padding: PaddingValues) -> Unit,
) {
val windowType = currentWindowType()
val navigationSuiteType = when {
navigationEntries.isNullOrEmpty() -> NavigationSuiteType.None
else -> { NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(currentWindowAdaptiveInfoV2()) }
}
val blurState = rememberBlurState()
CompositionLocalProvider(LocalBlurState provides blurState) {
when (navigationSuiteType) {
NavigationSuiteType.NavigationBar -> {
BottomBarScaffold(
modifier,
navigationEntries,
currentRoute,
appBar,
content,
scrollBehavior,
onSelectNavigation,
snackbarState
)
}
else -> {
DynamicNavigationScaffold(
modifier,
navigationSuiteType,
navigationEntries,
currentRoute,
onSelectNavigation,
scrollBehavior,
appBar,
snackbarState,
content
)
}
}
}
}
@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)
) {
NavigationSuiteScaffold(
navigationSuiteType = navigationSuiteType,
navigationItems = {
navigationEntries.orEmpty().forEach { entry ->
NavigationSuiteItem(
navigationSuiteType = navigationSuiteType,
selected = currentRoute == entry.route,
onClick = { onSelectNavigation?.invoke(entry.route) },
icon = { Icon(entry.painter, entry.title) },
label = {
when (navigationSuiteType) {
NavigationSuiteType.NavigationDrawer -> Text(entry.title)
}
},
modifier = Modifier.focusBorder(false, CircleShape)
)
}
}
) {
Scaffold(
bottomBar = {
if (windowType.isCompact) {
BottomBar(navigationEntries, currentRoute, onSelectNavigation)
}
},
topBar = {
appBar(scrollBehavior)
}, content = {
if (windowType.isExpanded && navigationEntries?.isNotEmpty() == true) {
ExpandedContent(
it,
navigationEntries,
currentRoute,
onSelectNavigation,
content
)
} else {
content(it)
}
}, modifier = if (scrollBehavior != null) {
modifier = if (scrollBehavior != null) {
modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
modifier
}, snackbarHost = {
snackbarState?.let { SnackbarHost(it) }
}
},
topBar = { appBar(scrollBehavior) },
snackbarHost = { snackbarState?.let { SnackbarHost(it) } },
content = content
)
}
}
@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>>?,
@@ -121,11 +198,8 @@ private fun <T : Route> BottomBar(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colorScheme.surfaceContainer
)
Brush.verticalGradient(
listOf(Color.Transparent, MaterialTheme.colorScheme.surfaceContainer)
)
)
.padding(8.dp)
@@ -155,37 +229,3 @@ private fun navBarContentColor(): Color {
NavigationBarDefaults.containerColor
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun <T : Route> ExpandedContent(
values: PaddingValues,
navigationEntries: List<NavigationEntry<T>>,
currentRoute: T?,
onSelectNavigation: ((T) -> Unit)?,
content: @Composable ((PaddingValues) -> Unit)
) {
val topPadding = values.calculateTopPadding()
val bottomPadding = values.calculateBottomPadding()
val endPadding = values.calculateEndPadding(LayoutDirection.Ltr)
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail(modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = topPadding)) {
navigationEntries.forEach { entry ->
NavigationRailItem(
selected = currentRoute == entry.route,
onClick = { onSelectNavigation?.invoke(entry.route) },
icon = { Icon(entry.painter, contentDescription = entry.title) }
)
}
}
Box(modifier = Modifier.weight(1f)) {
content(
PaddingValues(
top = topPadding,
end = endPadding,
bottom = bottomPadding
)
)
}
}
}
@@ -1,30 +0,0 @@
package ru.shadowsparky.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
@Composable
private fun currentWindowWidthDp(): Int {
val info = LocalWindowInfo.current
val density = LocalDensity.current
return with(density) { info.containerSize.width.toDp().value.toInt() }
}
@Composable
fun currentWindowType(): WindowType {
return widthToSizeClass(currentWindowWidthDp())
}
sealed interface WindowType {
object Compact : WindowType
object Expanded : WindowType
}
val WindowType.isExpanded: Boolean get() = this is WindowType.Expanded
val WindowType.isCompact: Boolean get() = this is WindowType.Compact
private fun widthToSizeClass(widthDp: Int): WindowType = when {
widthDp >= 840 -> WindowType.Expanded
else -> WindowType.Compact
}
@@ -4,12 +4,13 @@ import androiddev.libs.ui.generated.resources.Res
import androiddev.libs.ui.generated.resources.back
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.resources.painterResource
import ru.shadowsparky.ui.theme.focusBorder
@Composable
fun BackButton(doBack: () -> Unit) {
@@ -20,7 +21,8 @@ fun BackButton(doBack: () -> Unit) {
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(
0.1f
)
)
),
modifier = Modifier.focusBorder(shape = CircleShape)
) { Icon(painterResource(Res.drawable.back), contentDescription = null) }
}
@@ -0,0 +1,32 @@
package ru.shadowsparky.ui.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import ru.shadowsparky.ui.theme.focusBorder
@Composable
fun IconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = IconButtonDefaults.standardShape,
content: @Composable () -> Unit,
) {
IconButton(
onClick,
modifier.focusBorder(false, CircleShape),
enabled,
colors,
interactionSource,
shape,
content
)
}
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -17,6 +17,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
@@ -25,7 +26,8 @@ object ComponentDefaults {
@Composable
internal fun focusBorder(
modifier: Modifier,
needScale: Boolean
needScale: Boolean,
borderShape: Shape
): Modifier {
var isFocused by remember { mutableStateOf(false) }
val borderColor by animateColorAsState(
@@ -48,7 +50,7 @@ object ComponentDefaults {
scaleX = scale
scaleY = scale
clip = true
shape = RoundedCornerShape(12.dp)
shape = borderShape
}
} else {
modifier
@@ -57,12 +59,19 @@ object ComponentDefaults {
.border(
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(12.dp)
shape = borderShape
)
}
}
@Composable
fun Modifier.focusBorder(needScale: Boolean = true): Modifier {
return ComponentDefaults.focusBorder(this, needScale)
fun Modifier.focusBorder(
needScale: Boolean = true,
shape: Shape = RoundedCornerShape(12.dp)
): Modifier {
return ComponentDefaults.focusBorder(
this,
needScale,
shape
)
}