2.
Animating Custom Views
Written by Prateek Prasad
In Chapter 1, “Value & Object Animators”, you got an introduction to animations on Android and then wrote your very first view animations. Animating views bundled with the UI toolkit is one of the most common use cases you’ll encounter when writing apps; however, it’s far from the only one.
While building apps, the regular widgets often feel too limited in what they offer. Building a custom view is a fairly common option for Android developers who are writing custom, highly tailored behaviors. Custom views offer greater control over the view’s behavior, while giving you more power to fine-tune the parameters.
In this chapter, you’ll look at ways to introduce animations. In the process, you’ll also see how to use abstractions to expose only the necessary pieces of the animation control to the custom view consumers.
Setting up the project
Open this chapter’s starter project in Android Studio. Build and run.
The app is the same as in the last chapter, with one slight modification to the movie details screen.
Tap any movie to open the details screen. Scroll down and add the movie to favorites. You’ll notice that, while the operation is in the loading state, a progress bar appears in the button. This custom view, called FavoriteButton
, is located in the details package.
To get a better idea of that final animation, here are frame-by-frame screenshots. If you’d like to see it in action now, build and run the final project for the chapter.
Open view_favorite_button.xml from the res/layout folder and take a look at the layout for the custom view. Observe that it’s comprised of two views, an ExtendedFloatingActionButton
and a CircularProgressIndicator
. Now, open FavoriteButton.kt. You’ll notice that it extends from ConstraintLayout
. Also notice how setFavorite
switches the visibility of the progress indicator based on isFavorite
which is passed from MovieDetailsFragment
.
The progress indicator appears when showProgress
is called. Setting the movie’s favorite status via setFavorite
hides it again.
In this chapter, you’ll work on adding an animation for this state change. Now that you have your workspace set up, it’s time to get your hands dirty!
Building the progress animation
There are three key parts to the progress animation:
- Animating the width of the button.
- Animating the alpha of the progress bar.
- Animating the text size of the button.
First, you’ll focus on writing the width animation. In FavoriteButton.kt, add a new function called animateButton
. Then add the following code to the function:
private fun animateButton() {
//1
val initialWidth = binding.favoriteButton.measuredWidth
val finalWidth = binding.favoriteButton.measuredHeight
//2
val widthAnimator = ValueAnimator.ofInt(
initialWidth,
finalWidth
)
//3
widthAnimator.duration = 1000
//4
widthAnimator.addUpdateListener {
binding.favoriteButton.updateLayoutParams {
this.width = it.animatedValue as Int
}
}
//5
widthAnimator.start()
}
In the code above, you:
-
Set the
initialWidth
to the measured width of the button and thefinalWidth
to the measured height. You want the button to animate from its initial width to a final state where it becomes a circle. To convert a rectangle to a square, you need to make the width and height the same. By that same logic, since the button already has rounded corners, making the width and height the same makes it a circle. -
Instantiate a
ValueAnimator
using the staticofInt
, then pass theintialWidth
andfinalWidth
to it. -
Assign a 1,000 millisecond duration to the animator.
-
Add an
updateListener
to the animator and assign theanimatedValue
as the width of the button. -
Finally, you start the animation.
To make the animation run, call the newly created animateButton
animation from showProgress
by replacing the TODO
item.
animateButton()
Build and run the app. Try tapping on the button. The animation is far from finished and may feel a bit wonky right now, but the width animation should work as expected.
Next, you’ll address the alpha animation for the progress bar.
Adding the progress bar’s alpha animation
Right above the code where you declared widthAnimator
, add the following code:
val alphaAnimator = ObjectAnimator.ofFloat(
binding.progressBar,
"alpha",
0f,
1f
)
You created an ObjectAnimator
instance using ofFloat
. You then supplied the property to animate — in this case, alpha
— along with the start and final values for the animation.
Now, below widthAnimator.duration = 1000
add:
alphaAnimator.duration = 1000
You assigned a 1,000 millisecond duration for the animation.
Below widthAnimator.addUpdateListener
, add the code:
alphaAnimator.addUpdateListener {
binding.progressBar.alpha = it.animatedValue as Float
}
You added an updateListener
for the animation and updated the progressBar alpha value based on the animated value.
Next, make the progress bar visible by adding this code directly below the update listener you just added:
binding.progressBar.apply {
alpha = 0f
isVisible = true
}
You’ve prepared the progressBar
for the animation by making it visible and turning its initial alpha down to 0.
Lastly, the following right below widthAnimator.start()
:
alphaAnimator.start()
Now you started the animation.
Build and run. Tap Add to Favorites. The progress bar now becomes visible as the width change animation takes place.
At this point, the animation looks pretty good, but there’s an elephant in the room: While the animation starts as expected, the app ends up in a broken state once the animation completes.
You’ll fix that next.
Reversing the animations
To fix the button’s behavior, you need to make it go back to its initial state after the animation completes. In other words, you need to reverse the animation. Luckily for you, the animation APIs on Android make this pretty straightforward.
To reverse the animation, you need to maintain a reference of all the animators you created.
Right above the init{}
block in FavoriteButton.kt, add the following code:
private val animators = mutableListOf<ValueAnimator>()
Now, right before you start the animators in animateButton
, add the following:
animators.addAll(
listOf(
widthAnimator,
alphaAnimator
)
)
Next, create a new function called reverseAnimation
. Add the following code to reverse the animations:
private fun reverseAnimation() {
//1
animators.forEach { animation ->
//2
animation.reverse()
//3
if (animators.indexOf(animation) == animators.lastIndex) {
animation.doOnEnd {
animators.clear()
}
}
}
}
To unpack what’s going on here:
- You loop over the animations list one by one.
- Then, you call
reverse
on each animation. Yep! It’s that easy, just a single function call. - Once all the animations are reversed, you clear out the list to keep it tidy and to avoid adding duplicate references to it the next time the animation triggers.
All that’s left is to call reverseAnimation
. Do that from hideProgress
by replacing the TODO
item.
reverseAnimation()
All right, it’s time for the big reveal. Build and run. Tap Add to Favorites to trigger the animation. It looks pretty sweet, doesn’t it?
There’s a slightly rough edge here that would be great to polish out: When the text comes back into view, it feels very choppy!
As a final step, you’ll add a subtle animation to tie everything together.
Animating the text size
To animate the text size, add the code below at the top of animateButton
just below the declaration for finalWidth
:
val initialTextSize = binding.favoriteButton.textSize
This assigned the initialTextSize
to the button’s current text size.
Next, add the new ValueAnimator
right below the declaration for alphaAnimator
:
val textSizeAnimator = ValueAnimator.ofFloat(
initialTextSize,
0f
)
You just created a ValueAnimator
using the static ofFloat
, then passed it the initialTextSize
and a final text size value of 0
.
Below the code alphaAnimator.duration = 1000
, add:
textSizeAnimator.apply {
interpolator = OvershootInterpolator()
duration = 1000
}
You’ve added an OvershootInterpolator
to the animator and given it a 1,000 millisecond duration.
Below alphaAnimator.addUpdateListener
, insert the code:
textSizeAnimator.addUpdateListener {
binding.favoriteButton.textSize =
(it.animatedValue as Float) / resources.displayMetrics.density
}
This code assigned an updateListener
to the animator and updated the text size of the button using animatedValue
. Since the text size needs to be an sp
value, you have to divide the animated value by the screen density.
Right below alphaAnimator.start()
, add:
textSizeAnimator.start()
This will start the animation.
As a final step, add the textSizeAnimator
to the animators
list.
animators.addAll(
listOf(
widthAnimator,
alphaAnimator,
textSizeAnimator
)
)
Finally, build and run. You’ll notice a considerable difference in the feel of the animation.
This cleanup will make it easier for you (and others) to scan this function and quickly understand what’s going on.
You did a great job with the animation in this chapter! Even though it might seem like you just worked on a single button and its animation, this was an essential exercise on how to animate the different elements of a custom view, one step at a time.
Key points
- It’s easier to work with complex animation when you break them down into smaller individual animations.
- You can reverse animations by calling the reverse() function.
- You can create different value animations for different components of a custom view.
- When creating a value animator on text size it needs to be an
sp
value, so you can divide the animated value by the screen density. - A custom view’s animations can be fine tuned and abstracted away from the custom view’s consumer, keeping the code simple and the animations concise.