Untitled Composable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
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.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composables.compose.ripple.rememberRippleIndication
import com.composeunstyled.ProvideTextStyle
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.theme.buildThemeV2
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
val SlidingSelectionTheme = buildThemeV2 {
defaultContentColor = Color(0xFF20201E)
defaultIndication = rememberRippleIndication()
defaultTextStyle = TextStyle(fontSize = 14.sp)
}
@Composable
fun SlidingSelectionPill(
options: List<String>,
selectedIndex: Int,
onSelectionChange: (Int) -> Unit,
modifier: Modifier = Modifier,
containerColor: Color = Color(0xFFF0F0ED),
selectedColor: Color = Color(0xFFFFFFFF),
selectedTextColor: Color = Color(0xFF20201E),
unselectedTextColor: Color = Color(0xFF858580),
) {
require(options.isNotEmpty()) {
"Options cannot be empty"
}
require(selectedIndex in options.indices) {
"selectedIndex must be a valid option index"
}
val shape = RoundedCornerShape(100.dp)
BoxWithConstraints(
modifier = modifier
.fillMaxWidth()
.height(48.dp)
.clip(shape)
.background(containerColor)
.padding(4.dp)
) {
val itemWidth = maxWidth / options.size
val indicatorOffset by animateDpAsState(
targetValue = itemWidth * selectedIndex,
animationSpec = spring(
dampingRatio = 0.72f,
stiffness = Spring.StiffnessMediumLow
),
label = "selectionPillOffset"
)
// One shared indicator slides underneath every option.
Box(
modifier = Modifier
.offset(x = indicatorOffset)
.width(itemWidth)
.fillMaxHeight()
.clip(shape)
.background(selectedColor)
)
Row(modifier = Modifier.fillMaxSize()) {
options.forEachIndexed { index, option ->
val selected = index == selectedIndex
UnstyledButton(
onClick = {
if (index != selectedIndex) {
onSelectionChange(index)
}
},
indication = LocalIndication.current,
contentPadding = PaddingValues(0.dp),
modifier = Modifier
.weight(1f)
.fillMaxHeight()
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
ProvideTextStyle(
TextStyle(
fontSize = 14.sp,
fontWeight = if (selected) {
FontWeight.SemiBold
} else {
FontWeight.Medium
},
color = if (selected) {
selectedTextColor
} else {
unselectedTextColor
}
)
) {
Text(text = option)
}
}
}
}
}
}
}
@Preview
@Composable
fun SlidingSelectionPillPreview() {
var selectedIndex by remember { mutableIntStateOf(1) }
SlidingSelectionTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF8F8F6))
.padding(24.dp),
contentAlignment = Alignment.Center
) {
SlidingSelectionPill(
options = listOf("Day", "Week", "Month"),
selectedIndex = selectedIndex,
onSelectionChange = { selectedIndex = it }
)
}
}
}
build.gradle.kts
dependencies {
implementation("org.jetbrains.compose.foundation:foundation:1.12.0")
implementation("org.jetbrains.compose.ui:ui-tooling-preview:1.12.0")
implementation("com.composables:compose-uri-painter-wasm-js:1.2.0")
implementation("com.composables:composeunstyled:2.10.0")
implementation("com.composables:ripple-indication:1.1.0")
}