Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save iniyanmurugavel/91bcdc0a08182179ce4e9bb0b42d8579 to your computer and use it in GitHub Desktop.
Save iniyanmurugavel/91bcdc0a08182179ce4e9bb0b42d8579 to your computer and use it in GitHub Desktop.
AnimatedSelectableList
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
private object Dimens {
val cornerRadius = 16.dp
val borderWidth = 2.dp
val selectedBorderWidth = 4.dp
val verticalPadding = 24.dp
val horizontalPadding = 16.dp
val itemHeight = 80.dp
val itemSpacing = 12.dp
val textStartPadding = 24.dp
val headerVerticalPadding = 16.dp
val bottomContentPadding = 32.dp
const val animationDurationMillis = 400
}
@Composable
fun AnimatedSelectableList(modifier: Modifier = Modifier) {
val items = listOf(
"Stranger Things", "The Witcher", "Breaking Bad", "Dark", "Narcos", "Mindhunter",
"Squid Game", "Black Mirror", "House of Cards", "Money Heist", "Ozark", "The Crown",
"The Umbrella Academy", "You", "13 Reasons Why", "Sex Education", "Shadow and Bone",
"Alice in Borderland", "The Queen's Gambit", "Manifest", "Sweet Tooth",
"All of Us Are Dead", "Lupin", "Peaky Blinders", "Love, Death & Robots", "The Sandman"
)
val listState = rememberLazyListState()
var selectedIndex by remember { mutableStateOf(-1) }
var targetIndex by remember { mutableStateOf(-1) }
var isAnimating by remember { mutableStateOf(false) }
val itemPositions = remember { mutableStateMapOf<Int, IntOffset>() }
val borderOffset = remember { Animatable(IntOffset.Zero, IntOffset.VectorConverter) }
val borderSize = remember { Animatable(IntSize.Zero, IntSize.VectorConverter) }
val density = LocalDensity.current
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val itemWidthPx = with(density) { (screenWidth - Dimens.horizontalPadding * 2).roundToPx() }
val itemHeightPx = with(density) { Dimens.itemHeight.roundToPx() }
val targetSize = IntSize(itemWidthPx, itemHeightPx)
LaunchedEffect(targetIndex) {
if (targetIndex != selectedIndex && targetIndex != -1 && selectedIndex != -1) {
val from = itemPositions[selectedIndex]
val to = itemPositions[targetIndex]
if (from != null && to != null) {
isAnimating = true
borderOffset.snapTo(from)
borderSize.snapTo(targetSize)
launch { borderOffset.animateTo(to, tween(Dimens.animationDurationMillis)) }
launch { borderSize.animateTo(targetSize, tween(Dimens.animationDurationMillis)) }
delay(Dimens.animationDurationMillis.toLong())
selectedIndex = targetIndex
isAnimating = false
}
} else if (selectedIndex == -1 && targetIndex != -1) {
selectedIndex = targetIndex
}
}
Column(
modifier = modifier
.fillMaxSize()
.background(Color(0xFFF4F4F4))
.padding(vertical = Dimens.verticalPadding)
) {
Text(
text = "Choose an Item",
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold,
color = Color.Black
),
modifier = Modifier
.padding(vertical = Dimens.headerVerticalPadding)
.align(Alignment.CenterHorizontally)
)
Box {
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(Dimens.itemSpacing),
contentPadding = PaddingValues(bottom = Dimens.bottomContentPadding),
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(items) { index, title ->
val isSelected = index == selectedIndex && !isAnimating
val borderColor = if (isSelected) Color.Black else Color(0xFFDDDDDD)
Box(
modifier = Modifier
.padding(horizontal = Dimens.horizontalPadding)
.fillMaxWidth()
.height(Dimens.itemHeight)
.onGloballyPositioned {
itemPositions[index] = it.positionInParent().round()
}
.clip(RoundedCornerShape(Dimens.cornerRadius))
.background(Color.White)
.border(
width = if (isSelected) Dimens.selectedBorderWidth else Dimens.borderWidth,
color = borderColor,
shape = RoundedCornerShape(Dimens.cornerRadius)
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple()
) {
if (!isAnimating) targetIndex = index
},
contentAlignment = Alignment.CenterStart
) {
Text(
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Black,
modifier = Modifier.padding(start = Dimens.textStartPadding)
)
}
}
}
if (isAnimating) {
Box(
modifier = Modifier
.offset { borderOffset.value }
.size(
width = with(density) { borderSize.value.width.toDp() },
height = with(density) { borderSize.value.height.toDp() }
)
.border(
width = Dimens.selectedBorderWidth,
color = Color.Black,
shape = RoundedCornerShape(Dimens.cornerRadius)
)
)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment