feat: add Navigation

This commit is contained in:
2023-11-28 14:32:02 +08:00
parent 7ac4f4cb78
commit 9e583d383e

View File

@@ -23,4 +23,233 @@
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()
)