refactor: make TabPosition opened and use TabRow to manage TabIndicator

This commit is contained in:
2023-11-28 09:39:14 +08:00
parent 65e01c2e51
commit c6b72b6501

View File

@@ -19,7 +19,7 @@
* *
* This file is created by fankes on 2023/11/9. * This file is created by fankes on 2023/11/9.
*/ */
@file:Suppress("unused") @file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.highcapable.flexiui.component package com.highcapable.flexiui.component
@@ -102,25 +102,7 @@ fun TabRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TabColors = Tab.colors, colors: TabColors = Tab.colors,
style: TabStyle = Tab.style, style: TabStyle = Tab.style,
tabs: @Composable () -> Unit indicator: @Composable TabRow.() -> Unit = { TabIndicator(modifier = Modifier.tabIndicatorOffset()) },
) {
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 tabs: @Composable () -> Unit
) { ) {
TabStyleBox(modifier, colors, style) { TabStyleBox(modifier, colors, style) {
@@ -145,7 +127,7 @@ fun TabRow(
placeable.placeRelative(x = index * tabAverageWidth, y = 0) placeable.placeRelative(x = index * tabAverageWidth, y = 0)
} }
subcompose(TabSlots.Indicator) { subcompose(TabSlots.Indicator) {
TabIndicator(selectedTabIndex, colors, style, pagerState, tabPositions) indicator(TabRow(selectedTabIndex, colors, style, tabPositions))
}.forEach { }.forEach {
it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(x = 0, y = 0) it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(x = 0, y = 0)
} }
@@ -161,27 +143,7 @@ fun ScrollableTabRow(
colors: TabColors = Tab.colors, colors: TabColors = Tab.colors,
style: TabStyle = Tab.style, style: TabStyle = Tab.style,
scrollState: ScrollState = rememberScrollState(), scrollState: ScrollState = rememberScrollState(),
tabs: @Composable () -> Unit indicator: @Composable TabRow.() -> Unit = { TabIndicator(modifier = Modifier.tabIndicatorOffset()) },
) {
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 tabs: @Composable () -> Unit
) { ) {
TabStyleBox(modifier, colors, style) { TabStyleBox(modifier, colors, style) {
@@ -214,7 +176,7 @@ fun ScrollableTabRow(
tabLeft += placeables.width tabLeft += placeables.width
} }
subcompose(TabSlots.Indicator) { subcompose(TabSlots.Indicator) {
TabIndicator(selectedTabIndex, colors, style, pagerState, tabPositions) indicator(TabRow(selectedTabIndex, colors, style, tabPositions))
}.forEach { }.forEach {
it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(x = 0, y = 0) it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(x = 0, y = 0)
} }
@@ -271,23 +233,6 @@ fun Tab(
} }
} }
@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 @Composable
private fun TabStyleBox( private fun TabStyleBox(
modifier: Modifier, modifier: Modifier,
@@ -311,80 +256,107 @@ private fun rememberScrollableTabData(scrollState: ScrollState): ScrollableTabDa
return remember(scrollState, coroutineScope) { ScrollableTabData(scrollState, coroutineScope) } return remember(scrollState, coroutineScope) { ScrollableTabData(scrollState, coroutineScope) }
} }
private fun Modifier.tabIndicatorOffset( @Immutable
currentTabPosition: TabPosition, data class TabPosition(val left: Dp, val width: Dp, val tabWidth: Dp) {
indicatorWidth: Dp
) = composed( val right get() = left + width
inspectorInfo = debugInspectorInfo {
name = "tabIndicatorOffset" fun calculateCenter(currentWidth: Dp) = left + width / 2 - currentWidth / 2
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( @Stable
pagerState: PagerState, class TabRow internal constructor(
tabPositions: List<TabPosition>, val selectedTabIndex: Int,
indicatorWidth: Dp val colors: TabColors,
) = composed( val style: TabStyle,
inspectorInfo = debugInspectorInfo { val tabPositions: List<TabPosition>
name = "pagerTabIndicatorOffset"
properties["pagerState"] = pagerState
properties["tabPositions"] = tabPositions
properties["indicatorWidth"] = indicatorWidth
}
) { ) {
layout { measurable, constraints ->
// If there are no pages, nothing to show. @Composable
if (tabPositions.isEmpty()) return@layout layout(constraints.maxWidth, 0) {} fun TabIndicator(
val currentPage = minOf(tabPositions.lastIndex, pagerState.currentPage) modifier: Modifier = Modifier,
val currentTab = tabPositions[currentPage] color: Color = colors.indicatorColor,
val previousTab = tabPositions.getOrNull(currentPage - 1) height: Dp = style.indicatorHeight,
val nextTab = tabPositions.getOrNull(currentPage + 1) shape: Shape = style.indicatorShape
val currentWidth = indicatorWidth.orElse() ?: currentTab.tabWidth ) {
val nextWidth = indicatorWidth.orElse() ?: nextTab?.tabWidth ?: currentWidth Box(modifier.height(height).background(color, shape))
val previousWidth = indicatorWidth.orElse() ?: previousTab?.tabWidth ?: currentWidth }
val fraction = pagerState.currentPageOffsetFraction
// Calculate the width of the indicator from the current and next / previous tab. fun Modifier.tabIndicatorOffset(
val movableWidth = when { currentTabPosition: TabPosition = tabPositions[selectedTabIndex],
fraction > 0 && nextTab != null -> lerp(currentWidth, nextWidth, fraction) indicatorWidth: Dp = style.indicatorWidth
fraction < 0 && previousTab != null -> lerp(currentWidth, previousWidth, -fraction) ) = composed(
else -> currentWidth inspectorInfo = debugInspectorInfo {
}.roundToPx() name = "tabIndicatorOffset"
// Calculate the offset X of the indicator from the current and next / previous tab. properties["currentTabPosition"] = currentTabPosition
val movableOffsetX = when { properties["indicatorWidth"] = indicatorWidth
fraction > 0 && nextTab != null -> }
lerp(currentTab.calculateCenter(currentWidth), nextTab.calculateCenter(nextWidth), fraction) ) {
fraction < 0 && previousTab != null -> val currentWidth = indicatorWidth.orElse() ?: currentTabPosition.tabWidth
lerp(currentTab.calculateCenter(currentWidth), previousTab.calculateCenter(previousWidth), -fraction) val animatedWidh by animateDpAsState(
else -> currentTab.calculateCenter(currentWidth) targetValue = currentWidth,
}.roundToPx() animationSpec = tween(DefaultTabIndicatorDuration, easing = FastOutSlowInEasing)
val placeable = measurable.measure(
Constraints(
minWidth = movableWidth,
maxWidth = movableWidth,
minHeight = 0,
maxHeight = constraints.maxHeight
)
) )
val offsetY = maxOf(constraints.minHeight - placeable.height, 0) val animatedOffsetX by animateDpAsState(
val measureWidth = constraints.maxWidth targetValue = currentTabPosition.calculateCenter(currentWidth),
val measureHeight = maxOf(placeable.height, constraints.minHeight) animationSpec = tween(DefaultTabIndicatorDuration, easing = FastOutSlowInEasing)
layout(measureWidth, measureHeight) { placeable.placeRelative(movableOffsetX, offsetY) } )
fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(x = animatedOffsetX)
.width(animatedWidh)
}
fun Modifier.pagerTabIndicatorOffset(
pagerState: PagerState,
tabPositions: List<TabPosition> = this@TabRow.tabPositions,
indicatorWidth: Dp = style.indicatorWidth
) = 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) }
}
} }
} }
@@ -433,14 +405,6 @@ private class ScrollableTabData(private val scrollState: ScrollState, private va
} }
} }
@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 @Stable
private enum class TabSlots { Tabs, TabsAverage, Indicator } private enum class TabSlots { Tabs, TabsAverage, Indicator }