feat(Tabs): 自定义 TabRow 控件
This commit is contained in:
parent
322c68fef9
commit
8b358a080a
|
|
@ -9,7 +9,6 @@ import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
|
@ -33,6 +32,7 @@ import com.huanchengfly.tieba.post.ui.page.history.list.HistoryListUiEvent
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon
|
import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TabRow
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
|
||||||
import com.huanchengfly.tieba.post.utils.HistoryUtil
|
import com.huanchengfly.tieba.post.utils.HistoryUtil
|
||||||
import com.ramcosta.composedestinations.annotation.DeepLink
|
import com.ramcosta.composedestinations.annotation.DeepLink
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
|
@ -39,6 +38,7 @@ import com.huanchengfly.tieba.post.ui.page.main.explore.personalized.Personalize
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
|
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TabRow
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
||||||
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
|
@ -24,6 +23,7 @@ import com.huanchengfly.tieba.post.ui.page.main.notifications.list.Notifications
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
|
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TabRow
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.ProvideTextStyle
|
import androidx.compose.material.ProvideTextStyle
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
|
@ -101,6 +100,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.MyBackHandler
|
import com.huanchengfly.tieba.post.ui.widgets.compose.MyBackHandler
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TabRow
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.TopAppBarContainer
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TopAppBarContainer
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.picker.ListSinglePicker
|
import com.huanchengfly.tieba.post.ui.widgets.compose.picker.ListSinglePicker
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
|
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import androidx.compose.material.DropdownMenuItem
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.AccountCircle
|
import androidx.compose.material.icons.outlined.AccountCircle
|
||||||
|
|
@ -58,6 +57,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.MyLazyColumn
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.PromptDialog
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PromptDialog
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TabRow
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
|
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
|
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
package com.huanchengfly.tieba.post.ui.widgets.compose
|
package com.huanchengfly.tieba.post.ui.widgets.compose
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -10,22 +15,371 @@ import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.LocalContentColor
|
import androidx.compose.material.LocalContentColor
|
||||||
import androidx.compose.material.TabPosition
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.ScrollableTabRow
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
|
import androidx.compose.material.TabRowDefaults
|
||||||
|
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
||||||
|
import androidx.compose.material.contentColorFor
|
||||||
|
import androidx.compose.material.primarySurface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.UiComposable
|
||||||
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
|
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.dp
|
||||||
import androidx.compose.ui.unit.lerp
|
import androidx.compose.ui.unit.lerp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class holding onto state needed for [ScrollableTabRow]
|
||||||
|
*/
|
||||||
|
private class ScrollableTabData(
|
||||||
|
private val scrollState: ScrollState,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
private var selectedTab: Int? = null
|
||||||
|
|
||||||
|
fun onLaidOut(
|
||||||
|
density: Density,
|
||||||
|
edgeOffset: Int,
|
||||||
|
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, edgeOffset, tabPositions)
|
||||||
|
if (scrollState.value != calculatedOffset) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
scrollState.animateScrollTo(
|
||||||
|
calculatedOffset,
|
||||||
|
animationSpec = ScrollableTabRowScrollSpec
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
edgeOffset: Int,
|
||||||
|
tabPositions: List<TabPosition>,
|
||||||
|
): Int = with(density) {
|
||||||
|
val totalTabRowWidth = tabPositions.last().right.roundToPx() + edgeOffset
|
||||||
|
val visibleWidth = totalTabRowWidth - scrollState.maxValue
|
||||||
|
val tabOffset = left.roundToPx()
|
||||||
|
val scrollerCenter = visibleWidth / 2
|
||||||
|
val tabWidth = width.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [AnimationSpec] used when scrolling to a tab that is not fully visible.
|
||||||
|
*/
|
||||||
|
private val ScrollableTabRowScrollSpec: AnimationSpec<Float> = tween(
|
||||||
|
durationMillis = 250,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@UiComposable
|
||||||
|
fun TabRow(
|
||||||
|
selectedTabIndex: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color = MaterialTheme.colors.primarySurface,
|
||||||
|
contentColor: Color = contentColorFor(backgroundColor),
|
||||||
|
indicator: @Composable @UiComposable
|
||||||
|
(tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
|
||||||
|
TabRowDefaults.Indicator(
|
||||||
|
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
divider: @Composable @UiComposable () -> Unit =
|
||||||
|
@Composable {
|
||||||
|
TabRowDefaults.Divider()
|
||||||
|
},
|
||||||
|
tabs: @Composable @UiComposable () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.selectableGroup(),
|
||||||
|
color = backgroundColor,
|
||||||
|
contentColor = contentColor
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
|
||||||
|
val tabRowWidth = constraints.maxWidth
|
||||||
|
val tabMeasurables = subcompose(TabSlots.Tabs, tabs)
|
||||||
|
val tabCount = tabMeasurables.size
|
||||||
|
val tabWidth = (tabRowWidth / tabCount)
|
||||||
|
val tabPlaceables = tabMeasurables.map {
|
||||||
|
it.measure(constraints.copy(minWidth = tabWidth, maxWidth = tabWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
val tabRowHeight = tabPlaceables.maxByOrNull { it.height }?.height ?: 0
|
||||||
|
|
||||||
|
val tabPositions = List(tabCount) { index ->
|
||||||
|
TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(tabRowWidth, tabRowHeight) {
|
||||||
|
tabPlaceables.forEachIndexed { index, placeable ->
|
||||||
|
placeable.placeRelative(index * tabWidth, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
subcompose(TabSlots.Divider, divider).forEach {
|
||||||
|
val placeable = it.measure(constraints.copy(minHeight = 0))
|
||||||
|
placeable.placeRelative(0, tabRowHeight - placeable.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
subcompose(TabSlots.Indicator) {
|
||||||
|
indicator(tabPositions)
|
||||||
|
}.forEach {
|
||||||
|
it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://material.io/components/tabs#scrollable-tabs" class="external" target="_blank">Material Design scrollable tabs</a>.
|
||||||
|
*
|
||||||
|
* When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text
|
||||||
|
* labels and a larger number of tabs. They are best used for browsing on touch interfaces.
|
||||||
|
*
|
||||||
|
* 
|
||||||
|
*
|
||||||
|
* A ScrollableTabRow contains a row of [Tab]s, and displays an indicator underneath the currently
|
||||||
|
* selected tab. A ScrollableTabRow places its tabs offset from the starting edge, and allows
|
||||||
|
* scrolling to tabs that are placed off screen. For a fixed tab row that does not allow
|
||||||
|
* scrolling, and evenly places its tabs, see [TabRow].
|
||||||
|
*
|
||||||
|
* @param selectedTabIndex the index of the currently selected tab
|
||||||
|
* @param modifier optional [Modifier] for this ScrollableTabRow
|
||||||
|
* @param backgroundColor The background color for the ScrollableTabRow. Use [Color.Transparent] to
|
||||||
|
* have no color.
|
||||||
|
* @param contentColor The preferred content color provided by this ScrollableTabRow to its
|
||||||
|
* children. Defaults to either the matching content color for [backgroundColor], or if
|
||||||
|
* [backgroundColor] is not a color from the theme, this will keep the same value set above this
|
||||||
|
* ScrollableTabRow.
|
||||||
|
* @param edgePadding the padding between the starting and ending edge of ScrollableTabRow, and
|
||||||
|
* the tabs inside the ScrollableTabRow. This padding helps inform the user that this tab row can
|
||||||
|
* be scrolled, unlike a [TabRow].
|
||||||
|
* @param indicator the indicator that represents which tab is currently selected. By default this
|
||||||
|
* will be a [TabRowDefaults.Indicator], using a [TabRowDefaults.tabIndicatorOffset]
|
||||||
|
* modifier to animate its position. Note that this indicator will be forced to fill up the
|
||||||
|
* entire ScrollableTabRow, so you should use [TabRowDefaults.tabIndicatorOffset] or similar to
|
||||||
|
* animate the actual drawn indicator inside this space, and provide an offset from the start.
|
||||||
|
* @param divider the divider displayed at the bottom of the ScrollableTabRow. This provides a layer
|
||||||
|
* of separation between the ScrollableTabRow and the content displayed underneath.
|
||||||
|
* @param tabs the tabs inside this ScrollableTabRow. Typically this will be multiple [Tab]s. Each
|
||||||
|
* element inside this lambda will be measured and placed evenly across the TabRow, each taking
|
||||||
|
* up equal space.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@UiComposable
|
||||||
|
fun ScrollableTabRow(
|
||||||
|
selectedTabIndex: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color = MaterialTheme.colors.primarySurface,
|
||||||
|
contentColor: Color = contentColorFor(backgroundColor),
|
||||||
|
edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
|
||||||
|
indicator: @Composable @UiComposable
|
||||||
|
(tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
|
||||||
|
TabRowDefaults.Indicator(
|
||||||
|
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
divider: @Composable @UiComposable () -> Unit =
|
||||||
|
@Composable {
|
||||||
|
TabRowDefaults.Divider()
|
||||||
|
},
|
||||||
|
tabs: @Composable @UiComposable () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
color = backgroundColor,
|
||||||
|
contentColor = contentColor
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val scrollableTabData = remember(scrollState, coroutineScope) {
|
||||||
|
ScrollableTabData(
|
||||||
|
scrollState = scrollState,
|
||||||
|
coroutineScope = coroutineScope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SubcomposeLayout(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentSize(align = Alignment.CenterStart)
|
||||||
|
.horizontalScroll(scrollState)
|
||||||
|
.selectableGroup()
|
||||||
|
.clipToBounds()
|
||||||
|
) { constraints ->
|
||||||
|
val padding = edgePadding.roundToPx()
|
||||||
|
val tabConstraints = constraints.copy()
|
||||||
|
|
||||||
|
val tabPlaceables = subcompose(TabSlots.Tabs, tabs)
|
||||||
|
.map { it.measure(tabConstraints) }
|
||||||
|
|
||||||
|
var layoutWidth = padding * 2
|
||||||
|
var layoutHeight = 0
|
||||||
|
tabPlaceables.forEach {
|
||||||
|
layoutWidth += it.width
|
||||||
|
layoutHeight = maxOf(layoutHeight, it.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position the children.
|
||||||
|
layout(layoutWidth, layoutHeight) {
|
||||||
|
// Place the tabs
|
||||||
|
val tabPositions = mutableListOf<TabPosition>()
|
||||||
|
var left = padding
|
||||||
|
tabPlaceables.forEach {
|
||||||
|
it.placeRelative(left, 0)
|
||||||
|
tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp()))
|
||||||
|
left += it.width
|
||||||
|
}
|
||||||
|
|
||||||
|
// The divider is measured with its own height, and width equal to the total width
|
||||||
|
// of the tab row, and then placed on top of the tabs.
|
||||||
|
subcompose(TabSlots.Divider, divider).forEach {
|
||||||
|
val placeable = it.measure(
|
||||||
|
constraints.copy(
|
||||||
|
minHeight = 0,
|
||||||
|
minWidth = layoutWidth,
|
||||||
|
maxWidth = layoutWidth
|
||||||
|
)
|
||||||
|
)
|
||||||
|
placeable.placeRelative(0, layoutHeight - placeable.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The indicator container is measured to fill the entire space occupied by the tab
|
||||||
|
// row, and then placed on top of the divider.
|
||||||
|
subcompose(TabSlots.Indicator) {
|
||||||
|
indicator(tabPositions)
|
||||||
|
}.forEach {
|
||||||
|
it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollableTabData.onLaidOut(
|
||||||
|
density = this@SubcomposeLayout,
|
||||||
|
edgeOffset = padding,
|
||||||
|
tabPositions = tabPositions,
|
||||||
|
selectedTab = selectedTabIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Modifier] that takes up all the available width inside the [TabRow], and then animates
|
||||||
|
* the offset of the indicator it is applied to, depending on the [currentTabPosition].
|
||||||
|
*
|
||||||
|
* @param currentTabPosition [TabPosition] of the currently selected tab. This is used to
|
||||||
|
* calculate the offset of the indicator this modifier is applied to, as well as its width.
|
||||||
|
*/
|
||||||
|
fun Modifier.tabIndicatorOffset(
|
||||||
|
currentTabPosition: TabPosition,
|
||||||
|
): Modifier = composed(
|
||||||
|
inspectorInfo = debugInspectorInfo {
|
||||||
|
name = "tabIndicatorOffset"
|
||||||
|
value = currentTabPosition
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val currentTabWidth by animateDpAsState(
|
||||||
|
targetValue = currentTabPosition.width,
|
||||||
|
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
val indicatorOffset by animateDpAsState(
|
||||||
|
targetValue = currentTabPosition.left,
|
||||||
|
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
fillMaxWidth()
|
||||||
|
.wrapContentSize(Alignment.BottomStart)
|
||||||
|
.offset(x = indicatorOffset)
|
||||||
|
.width(currentTabWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class that contains information about a tab's position on screen, used for calculating
|
||||||
|
* where to place the indicator that shows which tab is selected.
|
||||||
|
*
|
||||||
|
* @property left the left edge's x position from the start of the [TabRow]
|
||||||
|
* @property right the right edge's x position from the start of the [TabRow]
|
||||||
|
* @property width the width of this tab
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
class TabPosition internal constructor(val left: Dp, val width: Dp) {
|
||||||
|
val right: Dp get() = left + width
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is TabPosition) return false
|
||||||
|
|
||||||
|
if (left != other.left) return false
|
||||||
|
return width == other.width
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = left.hashCode()
|
||||||
|
result = 31 * result + width.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "TabPosition(left=$left, right=$right, width=$width)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class TabSlots {
|
||||||
|
Tabs,
|
||||||
|
Divider,
|
||||||
|
Indicator
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PagerTabIndicator(
|
fun PagerTabIndicator(
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
tabPositions: List<TabPosition>
|
tabPositions: List<TabPosition>,
|
||||||
) {
|
) {
|
||||||
if (tabPositions.isNotEmpty()) {
|
if (tabPositions.isNotEmpty()) {
|
||||||
val tabWidth = 16.dp
|
val tabWidth = 16.dp
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue