mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-08 11:34:18 +08:00
feat: add Tab
This commit is contained in:
@@ -23,4 +23,469 @@
|
|||||||
|
|
||||||
package com.highcapable.flexiui.component
|
package com.highcapable.flexiui.component
|
||||||
|
|
||||||
// TODO: To be implemented
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
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.Stable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.lerp
|
||||||
|
import com.highcapable.flexiui.LocalColors
|
||||||
|
import com.highcapable.flexiui.LocalShapes
|
||||||
|
import com.highcapable.flexiui.LocalSizes
|
||||||
|
import com.highcapable.flexiui.extension.horizontal
|
||||||
|
import com.highcapable.flexiui.extension.orElse
|
||||||
|
import com.highcapable.flexiui.extension.status
|
||||||
|
import com.highcapable.flexiui.interaction.rippleClickable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class TabColors(
|
||||||
|
val indicatorColor: Color,
|
||||||
|
val selectedContentColor: Color,
|
||||||
|
val unselectedContentColor: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class TabStyle(
|
||||||
|
val contentPadding: PaddingValues,
|
||||||
|
val contentShape: Shape,
|
||||||
|
val indicatorWidth: Dp,
|
||||||
|
val indicatorHeight: Dp,
|
||||||
|
val indicatorShape: Shape
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabRow(
|
||||||
|
selectedTabIndex: Int = 0,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: TabColors = Tab.colors,
|
||||||
|
style: TabStyle = Tab.style,
|
||||||
|
tabs: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
TabRow(
|
||||||
|
selectedTabIndex = selectedTabIndex,
|
||||||
|
modifier = modifier,
|
||||||
|
colors = colors,
|
||||||
|
style = style,
|
||||||
|
pagerState = null,
|
||||||
|
tabs = tabs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabRow(
|
||||||
|
selectedTabIndex: Int = 0,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: TabColors = Tab.colors,
|
||||||
|
style: TabStyle = Tab.style,
|
||||||
|
pagerState: PagerState?,
|
||||||
|
tabs: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
TabStyleBox(modifier, colors, style) {
|
||||||
|
SubcomposeLayout(Modifier.fillMaxWidth().selectableGroup()) { constraints ->
|
||||||
|
val maximumConstraints = Constraints()
|
||||||
|
val tabRowWidth = constraints.maxWidth
|
||||||
|
val tabMeasurables = subcompose(TabSlots.Tabs, tabs)
|
||||||
|
val tabAverageMeasurables = subcompose(TabSlots.TabsAverage, tabs)
|
||||||
|
val tabCount = tabAverageMeasurables.size
|
||||||
|
val tabAverageWidth = (tabRowWidth / tabCount)
|
||||||
|
val tabPlaceables = tabMeasurables.map { it.measure(maximumConstraints) }
|
||||||
|
val tabAveragePlaceables = tabAverageMeasurables.map {
|
||||||
|
it.measure(constraints.copy(minWidth = tabAverageWidth, maxWidth = tabAverageWidth))
|
||||||
|
}
|
||||||
|
val tabRowHeight = tabAveragePlaceables.maxByOrNull { it.height }?.height ?: 0
|
||||||
|
val tabPositions = List(tabCount) { index ->
|
||||||
|
val tabWidth = tabPlaceables[index].width - style.contentPadding.horizontal.toPx()
|
||||||
|
TabPosition(tabAverageWidth.toDp() * index, tabAverageWidth.toDp(), tabWidth.toDp())
|
||||||
|
}
|
||||||
|
layout(tabRowWidth, tabRowHeight) {
|
||||||
|
tabAveragePlaceables.forEachIndexed { index, placeable ->
|
||||||
|
placeable.placeRelative(x = index * tabAverageWidth, y = 0)
|
||||||
|
}
|
||||||
|
subcompose(TabSlots.Indicator) {
|
||||||
|
TabIndicator(selectedTabIndex, colors, style, pagerState, tabPositions)
|
||||||
|
}.forEach {
|
||||||
|
it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(x = 0, y = 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScrollableTabRow(
|
||||||
|
selectedTabIndex: Int = 0,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: TabColors = Tab.colors,
|
||||||
|
style: TabStyle = Tab.style,
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
|
tabs: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
ScrollableTabRow(
|
||||||
|
selectedTabIndex = selectedTabIndex,
|
||||||
|
modifier = modifier,
|
||||||
|
colors = colors,
|
||||||
|
style = style,
|
||||||
|
pagerState = null,
|
||||||
|
scrollState = scrollState,
|
||||||
|
tabs = tabs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScrollableTabRow(
|
||||||
|
selectedTabIndex: Int = 0,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: TabColors = Tab.colors,
|
||||||
|
style: TabStyle = Tab.style,
|
||||||
|
pagerState: PagerState?,
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
|
tabs: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
TabStyleBox(modifier, colors, style) {
|
||||||
|
val scrollableTabData = rememberScrollableTabData(scrollState)
|
||||||
|
SubcomposeLayout(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.wrapContentSize(align = Alignment.CenterStart)
|
||||||
|
.horizontalScroll(scrollState)
|
||||||
|
.selectableGroup()
|
||||||
|
.clipToBounds()
|
||||||
|
) { constraints ->
|
||||||
|
val maximumConstraints = Constraints()
|
||||||
|
val tabMeasurables = subcompose(TabSlots.Tabs, tabs)
|
||||||
|
val tabAverageMeasurables = subcompose(TabSlots.TabsAverage, tabs)
|
||||||
|
val tabPlaceables = tabMeasurables.map { it.measure(maximumConstraints) }
|
||||||
|
val tabAveragePlaceables = tabAverageMeasurables.map { it.measure(constraints) }
|
||||||
|
var layoutWidth = 0
|
||||||
|
var layoutHeight = 0
|
||||||
|
tabAveragePlaceables.forEach {
|
||||||
|
layoutWidth += it.width
|
||||||
|
layoutHeight = maxOf(layoutHeight, it.height)
|
||||||
|
}
|
||||||
|
layout(layoutWidth, layoutHeight) {
|
||||||
|
var tabLeft = 0
|
||||||
|
val tabPositions = mutableListOf<TabPosition>()
|
||||||
|
tabAveragePlaceables.forEachIndexed { index, placeables ->
|
||||||
|
val tabWidth = tabPlaceables[index].width - style.contentPadding.horizontal.toPx()
|
||||||
|
placeables.placeRelative(x = tabLeft, y = 0)
|
||||||
|
tabPositions.add(TabPosition(tabLeft.toDp(), placeables.width.toDp(), tabWidth.toDp()))
|
||||||
|
tabLeft += placeables.width
|
||||||
|
}
|
||||||
|
subcompose(TabSlots.Indicator) {
|
||||||
|
TabIndicator(selectedTabIndex, colors, style, pagerState, tabPositions)
|
||||||
|
}.forEach {
|
||||||
|
it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(x = 0, y = 0)
|
||||||
|
}
|
||||||
|
scrollableTabData.onLaidOut(
|
||||||
|
density = this@SubcomposeLayout,
|
||||||
|
tabPositions = tabPositions,
|
||||||
|
selectedTab = selectedTabIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Tab(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selectedContentColor: Color = Color.Unspecified,
|
||||||
|
unselectedContentColor: Color = Color.Unspecified,
|
||||||
|
contentPadding: PaddingValues? = null,
|
||||||
|
contentShape: Shape? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val currentSelectedContentColor = selectedContentColor.orElse()
|
||||||
|
?: LocalTabSelectedContentColor.current.orElse() ?: Tab.colors.selectedContentColor
|
||||||
|
val currentUnselectedContentColor = unselectedContentColor.orElse()
|
||||||
|
?: LocalTabUnselectedContentColor.current.orElse() ?: Tab.colors.unselectedContentColor
|
||||||
|
val currentContentPadding = contentPadding ?: LocalTabContentPadding.current ?: Tab.style.contentPadding
|
||||||
|
val currentContentShape = contentShape ?: LocalTabContentShape.current ?: Tab.style.contentShape
|
||||||
|
val contentColor by animateColorAsState(if (selected) currentSelectedContentColor else currentUnselectedContentColor)
|
||||||
|
val contentStyle = LocalTextStyle.current.default(contentColor)
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalIconTint provides contentColor,
|
||||||
|
LocalTextStyle provides contentStyle
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.status(enabled)
|
||||||
|
.clip(currentContentShape)
|
||||||
|
.then(modifier)
|
||||||
|
.rippleClickable(
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onClick,
|
||||||
|
role = Role.Tab,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
.padding(currentContentPadding),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TabIndicator(
|
||||||
|
selectedTabIndex: Int,
|
||||||
|
colors: TabColors,
|
||||||
|
style: TabStyle,
|
||||||
|
pagerState: PagerState?,
|
||||||
|
tabPositions: List<TabPosition>
|
||||||
|
) {
|
||||||
|
val indicatorModifier = pagerState?.let { Modifier.pagerTabIndicatorOffset(it, tabPositions, style.indicatorWidth) }
|
||||||
|
?: Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex], style.indicatorWidth)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.then(indicatorModifier)
|
||||||
|
.height(style.indicatorHeight)
|
||||||
|
.background(colors.indicatorColor, style.indicatorShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TabStyleBox(
|
||||||
|
modifier: Modifier,
|
||||||
|
colors: TabColors,
|
||||||
|
style: TabStyle,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTabSelectedContentColor provides colors.selectedContentColor,
|
||||||
|
LocalTabUnselectedContentColor provides colors.unselectedContentColor,
|
||||||
|
LocalTabContentPadding provides style.contentPadding,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberScrollableTabData(scrollState: ScrollState): ScrollableTabData {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
return remember(scrollState, coroutineScope) { ScrollableTabData(scrollState, coroutineScope) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.tabIndicatorOffset(
|
||||||
|
currentTabPosition: TabPosition,
|
||||||
|
indicatorWidth: Dp
|
||||||
|
) = composed(
|
||||||
|
inspectorInfo = debugInspectorInfo {
|
||||||
|
name = "tabIndicatorOffset"
|
||||||
|
properties["currentTabPosition"] = currentTabPosition
|
||||||
|
properties["indicatorWidth"] = indicatorWidth
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val currentWidth = indicatorWidth.orElse() ?: currentTabPosition.tabWidth
|
||||||
|
val animatedWidh by animateDpAsState(
|
||||||
|
targetValue = currentWidth,
|
||||||
|
animationSpec = tween(DefaultTabIndicatorDuration, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
val animatedOffsetX by animateDpAsState(
|
||||||
|
targetValue = currentTabPosition.calculateCenter(currentWidth),
|
||||||
|
animationSpec = tween(DefaultTabIndicatorDuration, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
fillMaxWidth()
|
||||||
|
.wrapContentSize(Alignment.BottomStart)
|
||||||
|
.offset(x = animatedOffsetX)
|
||||||
|
.width(animatedWidh)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.pagerTabIndicatorOffset(
|
||||||
|
pagerState: PagerState,
|
||||||
|
tabPositions: List<TabPosition>,
|
||||||
|
indicatorWidth: Dp
|
||||||
|
) = composed(
|
||||||
|
inspectorInfo = debugInspectorInfo {
|
||||||
|
name = "pagerTabIndicatorOffset"
|
||||||
|
properties["pagerState"] = pagerState
|
||||||
|
properties["tabPositions"] = tabPositions
|
||||||
|
properties["indicatorWidth"] = indicatorWidth
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
layout { measurable, constraints ->
|
||||||
|
// If there are no pages, nothing to show.
|
||||||
|
if (tabPositions.isEmpty()) return@layout layout(constraints.maxWidth, 0) {}
|
||||||
|
val currentPage = minOf(tabPositions.lastIndex, pagerState.currentPage)
|
||||||
|
val currentTab = tabPositions[currentPage]
|
||||||
|
val previousTab = tabPositions.getOrNull(currentPage - 1)
|
||||||
|
val nextTab = tabPositions.getOrNull(currentPage + 1)
|
||||||
|
val currentWidth = indicatorWidth.orElse() ?: currentTab.tabWidth
|
||||||
|
val nextWidth = indicatorWidth.orElse() ?: nextTab?.tabWidth ?: currentWidth
|
||||||
|
val previousWidth = indicatorWidth.orElse() ?: previousTab?.tabWidth ?: currentWidth
|
||||||
|
val fraction = pagerState.currentPageOffsetFraction
|
||||||
|
// Calculate the width of the indicator from the current and next / previous tab.
|
||||||
|
val movableWidth = when {
|
||||||
|
fraction > 0 && nextTab != null -> lerp(currentWidth, nextWidth, fraction)
|
||||||
|
fraction < 0 && previousTab != null -> lerp(currentWidth, previousWidth, -fraction)
|
||||||
|
else -> currentWidth
|
||||||
|
}.roundToPx()
|
||||||
|
// Calculate the offset X of the indicator from the current and next / previous tab.
|
||||||
|
val movableOffsetX = when {
|
||||||
|
fraction > 0 && nextTab != null ->
|
||||||
|
lerp(currentTab.calculateCenter(currentWidth), nextTab.calculateCenter(nextWidth), fraction)
|
||||||
|
fraction < 0 && previousTab != null ->
|
||||||
|
lerp(currentTab.calculateCenter(currentWidth), previousTab.calculateCenter(previousWidth), -fraction)
|
||||||
|
else -> currentTab.calculateCenter(currentWidth)
|
||||||
|
}.roundToPx()
|
||||||
|
val placeable = measurable.measure(
|
||||||
|
Constraints(
|
||||||
|
minWidth = movableWidth,
|
||||||
|
maxWidth = movableWidth,
|
||||||
|
minHeight = 0,
|
||||||
|
maxHeight = constraints.maxHeight
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val offsetY = maxOf(constraints.minHeight - placeable.height, 0)
|
||||||
|
val measureWidth = constraints.maxWidth
|
||||||
|
val measureHeight = maxOf(placeable.height, constraints.minHeight)
|
||||||
|
layout(measureWidth, measureHeight) { placeable.placeRelative(movableOffsetX, offsetY) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
private class ScrollableTabData(private val scrollState: ScrollState, private val coroutineScope: CoroutineScope) {
|
||||||
|
|
||||||
|
private var selectedTab: Int? = null
|
||||||
|
|
||||||
|
fun onLaidOut(density: Density, tabPositions: List<TabPosition>, selectedTab: Int) {
|
||||||
|
// Animate if the new tab is different from the old tab, or this is called for the first
|
||||||
|
// time (i.e selectedTab is `null`).
|
||||||
|
if (this.selectedTab != selectedTab) {
|
||||||
|
this.selectedTab = selectedTab
|
||||||
|
tabPositions.getOrNull(selectedTab)?.let {
|
||||||
|
// Scrolls to the tab with [tabPosition], trying to place it in the center of the
|
||||||
|
// screen or as close to the center as possible.
|
||||||
|
val calculatedOffset = it.calculateTabOffset(density, tabPositions)
|
||||||
|
if (scrollState.value != calculatedOffset)
|
||||||
|
coroutineScope.launch {
|
||||||
|
scrollState.animateScrollTo(
|
||||||
|
value = calculatedOffset,
|
||||||
|
animationSpec = tween(DefaultTabIndicatorDuration, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the offset required to horizontally center the tab inside this TabRow.
|
||||||
|
* If the tab is at the start / end, and there is not enough space to fully centre the tab, this
|
||||||
|
* will just clamp to the min / max position given the max width.
|
||||||
|
*/
|
||||||
|
private fun TabPosition.calculateTabOffset(density: Density, tabPositions: List<TabPosition>): Int =
|
||||||
|
with(density) {
|
||||||
|
val totalTabRowWidth = tabPositions.last().right.roundToPx()
|
||||||
|
val visibleWidth = totalTabRowWidth - scrollState.maxValue
|
||||||
|
val tabOffset = left.roundToPx()
|
||||||
|
val scrollerCenter = visibleWidth / 2
|
||||||
|
val tabWidth = tabWidth.roundToPx()
|
||||||
|
val centeredTabOffset = tabOffset - (scrollerCenter - tabWidth / 2)
|
||||||
|
// How much space we have to scroll. If the visible width is <= to the total width, then
|
||||||
|
// we have no space to scroll as everything is always visible.
|
||||||
|
val availableSpace = (totalTabRowWidth - visibleWidth).coerceAtLeast(0)
|
||||||
|
return centeredTabOffset.coerceIn(0, availableSpace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private data class TabPosition(val left: Dp, val width: Dp, val tabWidth: Dp) {
|
||||||
|
|
||||||
|
val right get() = left + width
|
||||||
|
|
||||||
|
fun calculateCenter(currentWidth: Dp) = left + width / 2 - currentWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
private enum class TabSlots { Tabs, TabsAverage, Indicator }
|
||||||
|
|
||||||
|
object Tab {
|
||||||
|
val colors: TabColors
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = defaultTabColors()
|
||||||
|
val style: TabStyle
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = defaultTabStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LocalTabSelectedContentColor = compositionLocalOf { Color.Unspecified }
|
||||||
|
|
||||||
|
private val LocalTabUnselectedContentColor = compositionLocalOf { Color.Unspecified }
|
||||||
|
|
||||||
|
private val LocalTabContentPadding = compositionLocalOf<PaddingValues?> { null }
|
||||||
|
|
||||||
|
private val LocalTabContentShape = compositionLocalOf<Shape?> { null }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultTabColors() = TabColors(
|
||||||
|
indicatorColor = LocalColors.current.themePrimary,
|
||||||
|
selectedContentColor = LocalColors.current.themePrimary,
|
||||||
|
unselectedContentColor = LocalColors.current.textSecondary
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultTabStyle() = TabStyle(
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
horizontal = LocalSizes.current.spacingPrimary,
|
||||||
|
vertical = LocalSizes.current.spacingSecondary,
|
||||||
|
),
|
||||||
|
contentShape = when (LocalInAreaBox.current) {
|
||||||
|
true -> LocalAreaBoxShape.current
|
||||||
|
else -> LocalShapes.current.secondary
|
||||||
|
},
|
||||||
|
indicatorWidth = Dp.Unspecified,
|
||||||
|
indicatorHeight = DefaultTabIndicatorHeight,
|
||||||
|
indicatorShape = LocalShapes.current.tertiary
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val DefaultTabIndicatorDuration = 250
|
||||||
|
private val DefaultTabIndicatorHeight = 3.dp
|
Reference in New Issue
Block a user