mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-09 03:54:23 +08:00
feat: add Navigation
This commit is contained in:
@@ -23,4 +23,233 @@
|
|||||||
|
|
||||||
package com.highcapable.flexiui.component
|
package com.highcapable.flexiui.component
|
||||||
|
|
||||||
// TODO: To be implemented
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.highcapable.flexiui.LocalColors
|
||||||
|
import com.highcapable.flexiui.LocalSizes
|
||||||
|
import com.highcapable.flexiui.extension.orElse
|
||||||
|
import com.highcapable.flexiui.extension.status
|
||||||
|
import com.highcapable.flexiui.interaction.rippleClickable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class NavigationColors(
|
||||||
|
val indicatorColor: Color,
|
||||||
|
val selectedContentColor: Color,
|
||||||
|
val unselectedContentColor: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class NavigationStyle(
|
||||||
|
val padding: PaddingValues,
|
||||||
|
val contentSpace: Dp,
|
||||||
|
val contentPadding: PaddingValues,
|
||||||
|
val contentShape: Shape
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HorizontalNavigation(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: NavigationColors = Navigation.colors,
|
||||||
|
style: NavigationStyle = Navigation.style,
|
||||||
|
arrangement: Arrangement.Horizontal = Arrangement.SpaceBetween,
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
NavigationStyleBox(modifier, horizontal = true, colors, style) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().selectableGroup(),
|
||||||
|
horizontalArrangement = arrangement,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalNavigation(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: NavigationColors = Navigation.colors,
|
||||||
|
style: NavigationStyle = Navigation.style,
|
||||||
|
arrangement: Arrangement.Vertical = Arrangement.SpaceBetween,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
NavigationStyleBox(modifier, horizontal = false, colors, style) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().selectableGroup(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = arrangement,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavigationItem(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
horizontal: Boolean? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
colors: NavigationColors? = null,
|
||||||
|
contentSpace: Dp = Dp.Unspecified,
|
||||||
|
contentPadding: PaddingValues? = null,
|
||||||
|
contentShape: Shape? = null,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
text: @Composable (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val currentHorizontal = horizontal ?: LocalHorizontalNavigation.current
|
||||||
|
val currentColors = colors ?: LocalNavigationColors.current ?: Navigation.colors
|
||||||
|
val currentContentSpace = contentSpace.orElse() ?: LocalNavigationContentSpace.current.orElse() ?: Navigation.style.contentSpace
|
||||||
|
val currentContentPadding = contentPadding ?: LocalNavigationContentPadding.current ?: Navigation.style.contentPadding
|
||||||
|
val currentContentShape = contentShape ?: LocalNavigationContentShape.current ?: Navigation.style.contentShape
|
||||||
|
val animatedIndicatorColor by animateColorAsState(if (selected) currentColors.indicatorColor else Color.Transparent)
|
||||||
|
val animatedContentColor by animateColorAsState(if (selected) currentColors.selectedContentColor else currentColors.unselectedContentColor)
|
||||||
|
val currentContentStyle = LocalTextStyle.current.default(animatedContentColor)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.status(enabled)
|
||||||
|
.clip(currentContentShape)
|
||||||
|
.then(modifier)
|
||||||
|
.background(animatedIndicatorColor)
|
||||||
|
.rippleClickable(
|
||||||
|
enabled = enabled,
|
||||||
|
role = Role.Tab,
|
||||||
|
rippleColor = currentColors.indicatorColor,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(currentContentPadding)
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalIconTint provides animatedContentColor,
|
||||||
|
LocalTextStyle provides currentContentStyle
|
||||||
|
) {
|
||||||
|
if (currentHorizontal)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
text?.also { content ->
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = selected,
|
||||||
|
enter = expandHorizontally(),
|
||||||
|
exit = shrinkHorizontally()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Box(modifier = Modifier.width(currentContentSpace))
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
text?.also { content ->
|
||||||
|
Box(modifier = Modifier.height(currentContentSpace / 2))
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationStyleBox(
|
||||||
|
modifier: Modifier,
|
||||||
|
horizontal: Boolean,
|
||||||
|
colors: NavigationColors,
|
||||||
|
style: NavigationStyle,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.padding(style.padding)) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalHorizontalNavigation provides horizontal,
|
||||||
|
LocalNavigationColors provides colors,
|
||||||
|
LocalNavigationContentPadding provides style.contentPadding,
|
||||||
|
LocalNavigationContentShape provides style.contentShape,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Navigation {
|
||||||
|
val colors: NavigationColors
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = defaultNavigationColors()
|
||||||
|
val style: NavigationStyle
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = defaultNavigationStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LocalHorizontalNavigation = compositionLocalOf { true }
|
||||||
|
|
||||||
|
private val LocalNavigationColors = compositionLocalOf<NavigationColors?> { null }
|
||||||
|
|
||||||
|
private val LocalNavigationContentSpace = compositionLocalOf { Dp.Unspecified }
|
||||||
|
|
||||||
|
private val LocalNavigationContentPadding = compositionLocalOf<PaddingValues?> { null }
|
||||||
|
|
||||||
|
private val LocalNavigationContentShape = compositionLocalOf<Shape?> { null }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultNavigationColors() = NavigationColors(
|
||||||
|
indicatorColor = LocalColors.current.themeTertiary,
|
||||||
|
selectedContentColor = LocalColors.current.themePrimary,
|
||||||
|
unselectedContentColor = LocalColors.current.textSecondary
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultNavigationStyle() = NavigationStyle(
|
||||||
|
padding = when (LocalInAreaBox.current) {
|
||||||
|
true -> PaddingValues(0.dp)
|
||||||
|
else -> PaddingValues(
|
||||||
|
horizontal = LocalSizes.current.spacingPrimary,
|
||||||
|
vertical = LocalSizes.current.spacingSecondary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentSpace = LocalSizes.current.spacingSecondary,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
horizontal = LocalSizes.current.spacingPrimary,
|
||||||
|
vertical = LocalSizes.current.spacingSecondary
|
||||||
|
),
|
||||||
|
contentShape = withAreaBoxShape()
|
||||||
|
)
|
Reference in New Issue
Block a user