Introduction
- If you are looking for more Jetpack Compose/Android tutorials, please checkout my profile here on Dev.to
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)
}
}
If you are unfamiliar with the lazy components(Lazy Row) just know that inside the curly brackets
{}
it is implementing a type safe builder. Thetype-safe builder
is creating an instance of LazyListScope and it is what gives us access to theitemsIndexed()
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
}
}
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 thatmutableStateOf()
produces aMutableState
which inherits fromState
. As we know a recomposition is typically triggered by a change to aState
object. So anytime the value ofexpanded
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
)
)
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}
All
extraPadding.coerceAtLeast(0.dp)
is doing is making sure that there is no value less than 0 entered for the padding. By placingextraPadding
inside ofpadding()
all of the padding values will be automatically animated for us.clickable{ expanded = !expanded}
is how we make the card element clickable and change theexpanded
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
}
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
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)
again,
focusRequester()
allows us to make the card emit focus events andfocusable()
states where we are going to send those focus events to,interactionSource
(the prementioned pipeline).Lastly we have:
.clickable {focusRequester.requestFocus()}
- 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 bycollectIsFocusedAsState()
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)