Home Android & Kotlin Books Android Animations by Tutorials

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:

  1. 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.

  2. Instantiate a ValueAnimator using the static ofInt, then pass the intialWidth and finalWidth to it.

  3. Assign a 1,000 millisecond duration to the animator.

  4. Add an updateListener to the animator and assign the animatedValue as the width of the button.

  5. 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:

  1. You loop over the animations list one by one.
  2. Then, you call reverse on each animation. Yep! It’s that easy, just a single function call.
  3. 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

© 2022 Razeware LLC