I have this design that I am trying to follow:

When the user selects a duration I am trying to use a white box (not sure if that is the best way) to highlight the selection. The box shouldn't move instantly, it should animate from the previous position to the new position.
This is what I have so far:

A couple of issues:
5 mins are not visible.I think that I have to use animateDpAsState. When the user selects the i.e. 30 min I have to get the position of the Text's x position (top left of the text composable) and then set the target value to that x position. And then set the offset of the box to that animateDpState position.
This is my code so far:
var selectedIndex by remember { mutableIntStateOf(0) }
val items = listOf("5 min", "15 min", "30 min", "1 hour")
val animatedOffset by animateDpAsState(
targetValue = 0.dp, // This should be the selected text position
animationSpec = tween(durationMillis = 500),
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.background(
color = Color(0xff8138FF).copy(0.08f),
shape = RoundedCornerShape(12.dp),
)
.padding(4.dp),
) {
Row(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp)),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
items.forEachIndexed { index, item ->
Text(
fontWeight = FontWeight.W600,
fontSize = 16.sp,
text = item,
color = if (selectedIndex == index) Color.Black else Color.DarkGray,
modifier = Modifier
.clickable { selectedIndex = index }
.padding(horizontal = 16.dp),
)
}
}
Box(
modifier = Modifier
.width(100.dp) // Should be the same width as the text items
.height(48.dp)
.background(color = Color.White, shape = RoundedCornerShape(12.dp))
.offset(x = animatedOffset, y = 0.dp),
)
}
I would suggest that you use a TabRow to achieve this. It offers a nice animation out of the box and we can simply provide our own indicator Composable.
Please have a look at the following code:
@Composable
fun AnimatedChipSelector() {
val localDensity = LocalDensity.current
val tabsList = listOf("5 min", "15 min", "30 min", "1 hour")
var selectedTabIndex by remember { mutableIntStateOf(0) }
val tabWidths = remember { mutableStateListOf(-1, -1, -1, -1) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top
) {
TabRow(
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
selectedTabIndex = selectedTabIndex,
containerColor = Color(0xff8138FF).copy(0.08f),
indicator = { tabPositions ->
if (tabWidths.isNotEmpty()) { // only show Indicator after measurements are finished
Column(
modifier = Modifier
.tabIndicatorOffset(tabPositions[selectedTabIndex])
.fillMaxHeight()
.requiredWidth( with(localDensity) { tabWidths[selectedTabIndex].toDp() } )
.padding(vertical = 8.dp)
.background(
color = Color.White,
shape = RoundedCornerShape(12.dp)
)
) {}
}
},
divider = {}
) {
tabsList.forEachIndexed { tabIndex, tabName ->
FilterChip(
modifier = Modifier
.wrapContentSize()
.zIndex(2f)
.onGloballyPositioned { layoutCoordinates ->
tabWidths[tabIndex] = layoutCoordinates.size.width
},
selected = false,
shape = RoundedCornerShape(12.dp),
border = null,
onClick = { selectedTabIndex = tabIndex },
label = {
Text(
text = tabName,
textAlign = TextAlign.Center,
color = if (selectedTabIndex == tabIndex) Color.Black else Color.DarkGray,
)
}
)
}
}
}
}
I used the onGloballyPositioned Modifier to obtain the actual width of each single Tab, otherwise the Indicator width would be irrespectible of the actual Tab width.
Output:

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With