Compose Tab 指示器indicator自定义,Tab和Pager联动效果
简单改动indicator宽度
默认写法,可以设置高度、颜色,默认indicator 宽度充满
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
}
自定义写法仿照默认写法加以改动,调整指示器宽度
indicator = {
var currentTabPosition = it[index]
var minIndicatorWidth = 50.dp
val currentTabWidth by animateDpAsState(
targetValue = minIndicatorWidth,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
label = ""
)
val indicatorOffset by animateDpAsState(
targetValue = currentTabPosition.left.plus(
currentTabPosition.width.minus(
minIndicatorWidth
).div(2)
),
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
label = ""
)
TabRowDefaults.Indicator(
Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(x = indicatorOffset)
.width(currentTabWidth)
)
}
圆角背景指示器indicator,Tab和Pager联动效果,过程中的包名替换成自己的包名
用到的compose库中的源码如下
fun Modifier.tabIndicatorOffset(
currentTabPosition: com.ex.hellofigmatwo.TabPosition
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "tabIndicatorOffset"
value = currentTabPosition
}
) {
val currentTabWidth by animateDpAsState(
targetValue = currentTabPosition.width,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), label = ""
)
val indicatorOffset by animateDpAsState(
targetValue = currentTabPosition.left,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), label = ""
)
fillMaxWidth()
.wrapContentSize(Alignment.CenterStart)
.offset(x = indicatorOffset)
.width(currentTabWidth)
}
private class ScrollableTabData(
private val scrollState: ScrollState,
private val coroutineScope: CoroutineScope
) {
private var selectedTab: Int? = null
fun onLaidOut(
density: Density,
edgeOffset: Int,
tabPositions: List<com.ex.hellofigmatwo.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 = tween(
durationMillis = 250,
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 com.ex.hellofigmatwo.TabPosition.calculateTabOffset(
density: Density,
edgeOffset: Int,
tabPositions: List<com.ex.hellofigmatwo.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)
}
}
private enum class TabSlots {
Tabs,
Indicator
}
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
if (width != other.width) return false
return true
}
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)"
}
}
按照比重填充形式的TabRow自定义
@Composable
fun NioTabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
containerColor: Color = TabRowDefaults.containerColor,
contentColor: Color = TabRowDefaults.contentColor,
indicatorColor: Color = TabRowDefaults.contentColor,
tabs: @Composable () -> Unit
) {
Surface(
modifier = modifier.selectableGroup(),
color = containerColor,
contentColor = contentColor
) {
SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
val tabRowWidth = constraints.minWidth
val tabMeasurables = subcompose(com.ex.hellofigmatwo.TabSlots.Tabs, tabs)
val tabCount = tabMeasurables.size
val tabWidth = (tabRowWidth / tabCount)
val tabRowHeight = tabMeasurables.fold(initial = 0) { max, curr ->
maxOf(curr.maxIntrinsicHeight(tabWidth), max)
}
val tabPlaceables = tabMeasurables.map {
it.measure(
constraints.copy(
minWidth = tabWidth,
maxWidth = tabWidth,
minHeight = tabRowHeight
)
)
}
val tabPositions = List(tabCount) { index ->
TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
}
layout(tabRowWidth, tabRowHeight) {
subcompose(com.ex.hellofigmatwo.TabSlots.Indicator) {
Box(
Modifier
.tabIndicatorOffset(tabPositions[selectedTabIndex])
.fillMaxSize()
.background(
color = indicatorColor,
RoundedCornerShape(tabRowHeight.div(2))
)
)
}.forEach {
it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(0, 0)
}
tabPlaceables.forEachIndexed { index, placeable ->
placeable.placeRelative(index * tabWidth, 0)
}
}
}
}
}
引用方法以及呈现效果如下,调整高度、圆角、背景
val titles = listOf("客户分类", "客户分析")//, , "Tab Pager 3","Tab Pager 4", "Tab Pager 5"
var tabLayoutWidth = LocalConfiguration.current.screenWidthDp.times(2 / 3f)
var pageState = rememberPagerState()
var scope = rememberCoroutineScope()
Column {
NioTabRow(
selectedTabIndex = pageState.currentPage,
containerColor = Color.Transparent,
indicatorColor = MaterialTheme.colorScheme.primary, modifier = Modifier
.width(tabLayoutWidth.dp)
.wrapContentHeight()
.align(Alignment.CenterHorizontally)
.background(
Color.LightGray,
RoundedCornerShape(25.dp)
)
) {
titles.forEachIndexed { index, title ->
LeadingIconTab(
selected = pageState.currentPage == index,
onClick = {
scope.launch {
pageState.scrollToPage(index, 0f)
}
},
text = {
Text(
text = title, fontSize = TextUnit(13f, TextUnitType.Sp)
)
}, icon = {
Icon(
imageVector = Icons.Default.Face,
contentDescription = "",
modifier = Modifier.size(16.dp)
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.DarkGray, modifier = Modifier.height(40.dp)
)
}
}
val colors = listOf<Color>(
Color(0xFF807532),
Color(0xFFf8facd)
)
HorizontalPager(state = pageState, pageCount = titles.size) {
Box(
modifier = Modifier
.fillMaxSize()
.background(colors[it])
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Primary tab ${pageState.currentPage + 1} selected",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
可横向滑动的ScrollableTabRow自定义
@Composable
fun NioScrollableTabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
containerColor: Color = TabRowDefaults.containerColor,
contentColor: Color = TabRowDefaults.contentColor,
indicatorColor: Color = TabRowDefaults.contentColor,
edgePadding: Dp = 52.dp,
tabs: @Composable () -> Unit
) {
Surface(
modifier = modifier,
color = containerColor,
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 minTabWidth = 90.dp.roundToPx()
val padding = edgePadding.roundToPx()
val tabMeasurables = subcompose(com.ex.hellofigmatwo.TabSlots.Tabs, tabs)
val layoutHeight = tabMeasurables.fold(initial = 0) { curr, measurable ->
maxOf(curr, measurable.maxIntrinsicHeight(Constraints.Infinity))
}
val tabConstraints = constraints.copy(minWidth = minTabWidth, minHeight = layoutHeight)
val tabPlaceables = tabMeasurables
.map { it.measure(tabConstraints) }
val layoutWidth = tabPlaceables.fold(initial = padding * 2) { curr, measurable ->
curr + measurable.width
}
// Position the children.
layout(layoutWidth, layoutHeight) {
// Place the tabs
val tabPositions = mutableListOf<com.ex.hellofigmatwo.TabPosition>()
var left = padding
tabPlaceables.forEach {
tabPositions.add(
com.ex.hellofigmatwo.TabPosition(
left = left.toDp(),
width = it.width.toDp()
)
)
left += it.width
}
// The indicator container is measured to fill the entire space occupied by the tab
// row, and then placed on top of the divider.
subcompose(com.ex.hellofigmatwo.TabSlots.Indicator) {
var tp = tabPositions[selectedTabIndex]
Box(
Modifier
.tabIndicatorOffset(tp)
.fillMaxSize()
.background(
color = indicatorColor,
RoundedCornerShape(layoutHeight.div(2))
)
)
}.forEach {
it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0)
}
tabPositions.forEachIndexed { index, tabPosition ->
tabPlaceables[index].placeRelative(tabPosition.left.roundToPx(), 0)
}
scrollableTabData.onLaidOut(
density = this@SubcomposeLayout,
edgeOffset = padding,
tabPositions = tabPositions,
selectedTab = selectedTabIndex
)
}
}
}
}
引用方法以及呈现效果如下,调整左右边距、高度、圆角、背景、横向超出一屏滑动
val titles = listOf(
"Tab Pager 1",
"Tab Pager 2",
"Tab Pager 3",
"Tab Pager 4",
"Tab Pager 5",
"Tab Pager 6"
)
var tabLayoutWidth = LocalConfiguration.current.screenWidthDp.times(2 / 3f)
var pageState = rememberPagerState()
var scope = rememberCoroutineScope()
Column {
NioScrollableTabRow(
selectedTabIndex = pageState.currentPage,
containerColor = Color.Transparent,
indicatorColor = MaterialTheme.colorScheme.primary, modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(
Color.LightGray
), edgePadding = 12.dp
) {
titles.forEachIndexed { index, title ->
Tab(
selected = pageState.currentPage == index,
onClick = {
scope.launch {
pageState.scrollToPage(index, 0f)
}
},
text = {
Text(
text = title, fontSize = TextUnit(13f, TextUnitType.Sp)
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.DarkGray, modifier = Modifier.height(40.dp)
)
}
}
val colors = listOf<Color>(
Color(0xFF807532),
Color(0xFFf8facd),
Color(0xFFfd5e53),
Color(0xFFcec7a7),
Color(0xFFaec6cf), Color(0xFF2C5364)
)
HorizontalPager(state = pageState, pageCount = titles.size) {
Box(
modifier = Modifier
.fillMaxSize()
.background(colors[it])
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Primary tab ${pageState.currentPage + 1} selected",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
总结,先看组件有没有需要的效果,没有就点击进入源码中,将源码关联代码复制出来然后根据需要改造 需要学习更多组件用法请查看 Compose material3