Building a Custom Animated Expandable/Collapsed List Component with Jetpack Compose Using LazyColumn
One common feature in many applications is an expandable list, where users can click on an item to show more information. This article will guide you through building an animated expandable list using LazyColumn
in Jetpack Compose. We'll break down each component of the code, explore how state management works, and create a smooth, interactive experience.
Overview
To create an animated expandable list, we need the following:
- LazyColumn: A scrollable list layout that displays items.
- State Management: To track whether each item is expanded or collapsed.
- Animation: To add smooth transitions for expanding and collapsing items.
Let’s start by breaking down the code and explaining each part before putting it all together in the final solution.
Code Breakdown
1: Main Composable Function: AnimateExpandableList
The AnimateExpandableList
function is responsible for setting up the entire expandable list and managing the items' state.
data class ExpandableItem(
val title: String,
var isExpanded: Boolean = false
)
@Composable
fun AnimateExpandableList() {
val items = remember { List(20) { index -> ExpandableItem("Item #$index") } }
val expandedStates = remember { mutableStateListOf(*BooleanArray(items.size) { false }.toTypedArray()) }
val listState = rememberLazyListState()
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp),
state = listState
) {
itemsIndexed(items, key = { index, _ -> index }) { index, item ->
ExpandableListItem(
item = item,
index = index,
isExpanded = expandedStates[index],
onExpandedChange = { expandedStates[index] = it }
)
}
}
}
Explanation:
1. Creating Items (val items
):
remember { List(20) { index -> ExpandableItem("Item #$index") } }
: This line creates a list of 20 items, each represented by anExpandableItem
object. Theremember
function ensures that the list is not regenerated on every recomposition, maintaining consistency across UI changes.
2. State Tracking (val expandedStates
):
val expandedStates = remember { mutableStateListOf(*BooleanArray(items.size) { false }.toTypedArray()) }
: This list keeps track of whether each item is expanded or collapsed.remember
: Preserves the state across recompositions.mutableStateListOf()
: Creates an observable list that triggers recompositions when updated.BooleanArray(items.size) { false }
: Initializes the list withfalse
, meaning all items are initially collapsed.*
and.toTypedArray()
: The spread operator (*
) unpacks the array into individual elements, and.toTypedArray()
converts theBooleanArray
into a format compatible withmutableStateListOf()
.
3. List State (val listState
):
rememberLazyListState()
: Keeps track of the scroll position of the list, allowing us to control scrolling behavior if needed.
4. LazyColumn
Setup:
LazyColumn(...)
: Creates a vertical list that fills the available space.verticalArrangement = Arrangement.spacedBy(12.dp)
: Adds 12 dp of spacing between each list item.contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp)
: Adds padding at the top and bottom to prevent clipping.itemsIndexed(...)
: This function iterates through the items, passing each item's index and content to theExpandableListItem
composable. TheexpandedStates
list keeps track of the expanded state of each item.
2: Expandable Item Composable: ExpandableListItem
The ExpandableListItem
composable is responsible for rendering each item in the list and managing its expanded or collapsed state.
@Composable
fun ExpandableListItem(
item: ExpandableItem,
index: Int,
isExpanded: Boolean,
onExpandedChange: (Boolean) -> Unit
) {
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() }
val rotationAngle by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
Column(
modifier = Modifier
.fillMaxWidth()
.shadow(4.dp, shape = RoundedCornerShape(12.dp))
.background(color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(12.dp))
.clickable(interactionSource = interactionSource, indication = null) {
onExpandedChange(!isExpanded)
}
.padding(16.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = item.title,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = if (isExpanded) "Collapse" else "Expand",
modifier = Modifier.graphicsLayer(rotationZ = rotationAngle)
)
}
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
) {
Text(
text = "Details about ${item.title}:",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ac ante sit amet est commodo placerat. Suspendisse potenti.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
Toast.makeText(context, "More Info clicked for item #$index", Toast.LENGTH_SHORT).show()
},
modifier = Modifier.align(Alignment.End)
) {
Text("More Info")
}
}
}
}
}
Explanation:
1. Context and Interaction Handling:
val context = LocalContext.current
: Gives access to the current context, which is needed for actions like showing a toast.val interactionSource = remember { MutableInteractionSource() }
: Tracks user interactions.
2. Rotation Animation (val rotationAngle
):
animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
: Animates the rotation of the arrow icon. When the item is expanded, the icon rotates to 180°; otherwise, it returns to 0°.
3. Item Layout:
Column(...)
: The main container for each list item.Modifier.clickable { onExpandedChange(!isExpanded) }
: Handles the click event to toggle the expanded state.Row(...)
: Contains the title and the rotating arrow icon.Icon(...)
andText(...)
: Display the item icon and title.
4. Animated Visibility (AnimatedVisibility
):
AnimatedVisibility(visible = isExpanded, ...)
: Controls the visibility of the item's expanded content.enter
andexit
: Define the animations for expanding and collapsing, using effects likeexpandVertically()
,fadeIn()
,shrinkVertically()
, andfadeOut()
for a smooth transition.
5. Expanded Content:
- It contains additional details and a button labeled “More Info”. When the button is clicked, a toast message is displayed.
Summary
Creating a custom animated expandable list with Jetpack Compose involves managing state effectively and using animations to enhance the user experience. The LazyColumn
provides a scrollable list, while mutableStateListOf
allows for efficient tracking of each item's expanded state. The ExpandableListItem
component uses AnimatedVisibility
and rotation animations to create a dynamic and interactive UI.
Connect with Me on LinkedIn
If you found this article helpful and want to stay updated with more insights and tips on Android development, Jetpack Compose, and other tech topics, feel free to connect with me on LinkedIn. I regularly publish articles, share my experiences, and engage with the developer community. Your feedback and interaction are always welcome!