DEV Community 👩‍💻👨‍💻

Cover image for Jetpack Compose quicky: List click animation with Jetpack Compose in Android
Tristan Elliott
Tristan Elliott

Posted on

Jetpack Compose quicky: List click animation with Jetpack Compose in Android

Introduction

  • Anytime I create something Jetpack Compose related I will post it in this series. The series will not be in order so feel free to browse around

YouTube video

What we are going to be making

  • So today we are going to be animating clicks on Lazy Rows in Jetpack compose. We are going to implement two versions of the click:

1) normal version: Official google documentation shows you how to implement this version HERE

2) focused version : in this version we will make it so only one animation will be triggered at a time.

  • If you are wondering what version you are looking for, check out the video for a demonstration

normal version

  • So we start off with a normal Lazy Row like so:
val items = (1..168).map { "Item $it" }
LazyRow(
                horizontalArrangement = Arrangement.SpaceEvenly,
                state = rememberLazyListState()
            ) {

                itemsIndexed(items) { index, item ->
                    CardShown(item)
                }

            }

Enter fullscreen mode Exit fullscreen mode
  • If you are unfamiliar with the lazy components(Lazy Row) just know that inside the curly brackets {} it is implementing a type safe builder. The type-safe builder is creating an instance of LazyListScope and it is what gives us access to the itemsIndexed() method.

  • Now we can get into animating the click:

@Composable
fun CardShown(item: String){
var expanded by remember { mutableStateOf(false) }

 val extraPadding by animateDpAsState(
        if (!isFocused ) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
Card(
        backgroundColor = Color.LightGray,
        modifier = Modifier
            .height(300.dp)
            .width(160.dp)
            .padding(extraPadding.coerceAtLeast(0.dp)
            .clickable{ expanded = !expanded}

    ) {
        Text(item) // card's content
    }

}

Enter fullscreen mode Exit fullscreen mode
  • First we create the state that is going to hold a true or false value depending if the Card has been clicked of not, var expanded by remember { mutableStateOf(false) }. The important thing to remember is that mutableStateOf() produces a MutableState which inherits from State. As we know a recomposition is typically triggered by a change to a State object. So anytime the value of expanded is changed the whole card will undergo a recomposition.

  • Next we have the code that is doing the actual animation:

val extraPadding by animateDpAsState(
        targetValue = if (!expanded ) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
Enter fullscreen mode Exit fullscreen mode
  • As the documentation states, animateDpAsState is a fire-and-forget animation function for dp. When the provided targetValue is changed, the animation will run automatically.

  • spring() : creates a physic-based animation between start and end values and it takes in 2 parameters. 1)dampingRatio, defines how bouncy the spring should be. 2)stiffness, defines how fast the spring should run towards the end value.

  • Lastly we simply have to modify the Card's modifier to make card clickable:

            .padding(extraPadding.coerceAtLeast(0.dp)
            .clickable{ expanded = !expanded}

Enter fullscreen mode Exit fullscreen mode
  • All extraPadding.coerceAtLeast(0.dp) is doing is making sure that there is no value less than 0 entered for the padding. By placing extraPadding inside of padding() all of the padding values will be automatically animated for us. clickable{ expanded = !expanded} is how we make the card element clickable and change the expanded variable which triggers the recomposition and the animation.

  • The official documentation on how to create and implement the code above is HERE

focused version

  • Now this implementation is of my own creation, it adds a focusable element to the card and only allow one card to be open at a time. The code looks like this:
@Composable
fun CardShown(item: String){

    val focusRequester = remember { FocusRequester() }

    val interactionSource = remember { MutableInteractionSource() }
    val isFocused = interactionSource.collectIsFocusedAsState().value

 val extraPadding by animateDpAsState(
        targetValue = if (!isFocused ) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
Card(
        backgroundColor = Color.LightGray,
        modifier = Modifier
            .focusRequester(focusRequester)// needed to make work
            .focusable(interactionSource = interactionSource)
            .height(300.dp)
            .width(160.dp)
            .padding(extraPadding.coerceAtLeast(0.dp))
            .clickable {focusRequester.requestFocus()}
) {
        Text(item) // card's content
    }

Enter fullscreen mode Exit fullscreen mode
  • Basically the whole idea with the code above is we want one object to be open when we click it. But when we click another object the open object closes and the clicked object opens.

  • To achieve this we need to get the state of the element when it is clicked and the state of it when it is not clicked. It all starts with these 3 variables:

 val focusRequester = remember { FocusRequester() }

    val interactionSource = remember { MutableInteractionSource() }
    val isFocused = interactionSource.collectIsFocusedAsState().value

Enter fullscreen mode Exit fullscreen mode
  • focusRequester is how we can interact with the focus state of an element. interactionSource is a little more complicated and if you want to find out more about interactions, check out the documentation, HERE. But basically it acts as a pipe line that we can listen to for changes in focus events. isFocused is where we listen for those changes in focus events.

  • The we have:

        .focusRequester(focusRequester)
        .focusable(interactionSource = interactionSource)

Enter fullscreen mode Exit fullscreen mode
  • again, focusRequester() allows us to make the card emit focus events and focusable() states where we are going to send those focus events to, interactionSource(the prementioned pipeline).

  • Lastly we have:

.clickable {focusRequester.requestFocus()} 

Enter fullscreen mode Exit fullscreen mode
  • The above code is us forcing focus onto the object when it get clicked, which will emit a focus event to the interactionSource which is being listened to by collectIsFocusedAsState() and a change will cause a animation to take place

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (0)

Advice For Junior Developers

Advice from a career of 15+ years for new and beginner developers just getting started on their journey.