TabIndicatorScope



Scope for the composable used to render a Tab indicator, this can be used for more complex indicators requiring layout information about the tabs like TabRowDefaults.PrimaryIndicator and TabRowDefaults.SecondaryIndicator

Summary

Public functions

Modifier

A layout modifier that provides tab positions, this can be used to animate and layout a TabIndicator depending on size, position, and content size of each Tab.

Cmn
Modifier
Modifier.tabIndicatorOffset(
    selectedTabIndex: Int,
    matchContentSize: Boolean
)

A Modifier that follows the default offset and animation

Cmn

Public functions

tabIndicatorLayout

fun Modifier.tabIndicatorLayout(
    measure: MeasureScope.(Measurable, Constraints, List<TabPosition>) -> MeasureResult
): Modifier

A layout modifier that provides tab positions, this can be used to animate and layout a TabIndicator depending on size, position, and content size of each Tab.

import androidx.compose.animation.animateColor
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.TabPosition
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val colors = listOf(
    MaterialTheme.colorScheme.primary,
    MaterialTheme.colorScheme.secondary,
    MaterialTheme.colorScheme.tertiary,
)
var startAnimatable by remember { mutableStateOf<Animatable<Dp, AnimationVector1D>?>(null) }
var endAnimatable by remember { mutableStateOf<Animatable<Dp, AnimationVector1D>?>(null) }
val coroutineScope = rememberCoroutineScope()
val indicatorColor: Color by animateColorAsState(colors[index % colors.size], label = "")

Box(
    Modifier
        .tabIndicatorLayout { measurable: Measurable, constraints: Constraints,
            tabPositions: List<TabPosition> ->
            val newStart = tabPositions[index].left
            val newEnd = tabPositions[index].right
            val startAnim = startAnimatable ?: Animatable(newStart, Dp.VectorConverter)
                .also { startAnimatable = it }

            val endAnim = endAnimatable ?: Animatable(newEnd, Dp.VectorConverter)
                .also { endAnimatable = it }

            if (endAnim.targetValue != newEnd) {
                coroutineScope.launch {
                    endAnim.animateTo(
                        newEnd,
                        animationSpec =
                        if (endAnim.targetValue < newEnd) {
                            spring(dampingRatio = 1f, stiffness = 1000f)
                        } else {
                            spring(dampingRatio = 1f, stiffness = 50f)
                        }

                    )
                }
            }

            if (startAnim.targetValue != newStart) {
                coroutineScope.launch {
                    startAnim.animateTo(
                        newStart,
                        animationSpec =
                        // Handle directionality here, if we are moving to the right, we
                        // want the right side of the indicator to move faster, if we are
                        // moving to the left, we want the left side to move faster.
                        if (startAnim.targetValue < newStart) {
                            spring(dampingRatio = 1f, stiffness = 50f)
                        } else {
                            spring(dampingRatio = 1f, stiffness = 1000f)
                        }

                    )
                }
            }

            val indicatorEnd = endAnim.value.roundToPx()
            val indicatorStart = startAnim.value.roundToPx()

            // Apply an offset from the start to correctly position the indicator around the tab
            val placeable = measurable.measure(
                constraints.copy(
                    maxWidth = indicatorEnd - indicatorStart,
                    minWidth = indicatorEnd - indicatorStart,
                )
            )
            layout(constraints.maxWidth, constraints.maxHeight) {
                placeable.place(indicatorStart, 0)
            }
        }
        .padding(5.dp)
        .fillMaxSize()
        .drawWithContent {
            drawRoundRect(
                color = indicatorColor,
                cornerRadius = CornerRadius(5.dp.toPx()),
                style = Stroke(width = 2.dp.toPx())
            )
        }
)

tabIndicatorOffset

fun Modifier.tabIndicatorOffset(
    selectedTabIndex: Int,
    matchContentSize: Boolean = false
): Modifier

A Modifier that follows the default offset and animation

Parameters
selectedTabIndex: Int

the index of the current selected tab

matchContentSize: Boolean = false

this modifier can also animate the width of the indicator
to match the content size of the tab.