From 9e583d383e5c59ee7c79fe3e4b293d330ce3795b Mon Sep 17 00:00:00 2001 From: fankesyooni Date: Tue, 28 Nov 2023 14:32:02 +0800 Subject: [PATCH] feat: add Navigation --- .../flexiui/component/Navigation.kt | 231 +++++++++++++++++- 1 file changed, 230 insertions(+), 1 deletion(-) diff --git a/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Navigation.kt b/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Navigation.kt index 2cfbfd9..195bd4b 100644 --- a/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Navigation.kt +++ b/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Navigation.kt @@ -23,4 +23,233 @@ package com.highcapable.flexiui.component -// TODO: To be implemented \ No newline at end of file +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 { null } + +private val LocalNavigationContentSpace = compositionLocalOf { Dp.Unspecified } + +private val LocalNavigationContentPadding = compositionLocalOf { null } + +private val LocalNavigationContentShape = compositionLocalOf { 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() +) \ No newline at end of file