update scaffolds
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user