Animating RecyclerView’s ItemDecoration

Case study of implementing animated ItemDecoration in RecyclerView

Konrad Kowalewski
DaftMobile Blog

--

Hi everyone! For a long time, the DaftMobile blog was an enclave of iOS development, a fabulous land of looong compile times and endless App Store review hassle. Today we’re starting another fascinating journey to the kingdom of fragmentation, multitude of different screen sizes and the home for a little green robot — Android.

Our food delivery app, RocketLuncher, is the first DaftMobile’s product to target two most popular mobile platforms: iOS and Android. During the development process there came a time when we had to introduce multiple kinds of dishes for our users to choose from.To help indicate what was being selected or deselected we went for a custom selection animation. Every single dish was to be represented as a circle with an appropriate icon, which would then animate in and out of the screen based on user actions. Simple!

To achieve this effect we created views consisting of an ImageView with ShapeDrawable as a background, put them into a RecyclerView and then animated them using ItemAnimator. Remember to always call dispatchXyzFinished() when overriding animateXyz() in your ItemAnimator, because not doing so leads to weird view behaviour.

Behold the lunch sets!

Then our designers came up with an enhancement: it would be nice to have dishes that form a lunch set to merge their circles together into an elongated pill. They even provided us with some detailed specs:

Our first thought was that we didn’t have enough mana to do that kind of animagic, since we used all of it on dancing circles. However, our product owner didn’t believe any mana nonsense, so we began considering available options. We were already using RecyclerView with custom ItemAnimator to animate dish circles, so it was natural to explore RecyclerView’s capabilities further.

ItemDecoration to the rescue

The first idea we had was to create a different item type for a lunch set. We would then use onAnimateChange() callback in ItemAnimator to create a nice transition from dish to set and vice versa. However, things got more complicated when we considered the possibility of a lunch set consisting of more that two dishes. That would require another nested RecyclerView with its own animations. There must have been a better solution.

A breakthrough came when we realized that we can treat the item’s circle as a decoration rather than part of the view itself, leading us to ItemDecoration class.

What’s important to know, ItemDecoration object draws on one canvas associated with the whole RecyclerView, as opposed to e.g. ItemAnimator, which animates every child independently. So the idea was that ItemDecoration object would draw a circle around each single dish, and a pill around dishes making a set.

There was yet one thing to discover — how do you animate decoration?

Do it the old way…

Android API docs say nothing about animating ItemDecoration. Old Android animation API, as well as newer Animators, weren’t an option, as they are based on animating certain View properties, and in ItemDecoration there is no obvious property to be animated. So we kept on digging.

When implementing ItemDecoration you may implement onDraw() and onDrawOver() methods, which basically do the same thing, but in different points in time (the former draws before items are drawn, while the latter — after).

Those methods are invoked every time RecyclerView redraws its children. As it turned out, android redraws recycler on each frame its items are animated through ItemAnimator. So we could draw circles and pills on a frame-by-frame basis.

override fun onDrawOver(canvas: Canvas,
parent: RecyclerView,
state: RecyclerView.State) {
// update animation progress
// update items state
// draw appropriate stuff
}

But we needed one more thing: a way of telling item decorator what state of animation to draw. More specifically, we needed a value that would change during animation, which brought us back to ValueAnimator.

…using new(ish) API

ValueAnimator holds a value which is changed in the course of ongoing animation (and may be passed on to listeners). Our ItemDecoration would need one of those to get current progress of dish entry/exit animation and draw appropriate in-between stage of circle-to-pill transition. The ValueAnimator needed to be in sync with dish icons Animators, so we made ItemAnimator responsible for managing it. ValueAnimator was then exposed to ItemDecoration to be queried for current animation progress.

private fun updateAnimationProgress(animator: OurItemAnimator) {
if (animator.isRunning)
animationProgress = animator.animationProgress
else
animationProgress = 1f
}

From this value we then derive alpha for entering/exiting circle decoration or length for a transitioning pill. Notice how we use 1f value to draw fully extended pill or fully opaque circle when the last, non-animated (hence animator.isRunning) frame is drawn.

Managing state(s)

The trickiest part of implementing this feature was properly handling the transient state of view. More precisely, every time the ItemDecoration methods were called it meant that the layout is animating and each component of RecyclerView knows only the current state, the one it is animating into. None of the classes holds any “historical” info, so our ItemDecoration object had to keep the previous state information internally. For example, when adding a dish that would create a lunch set with already selected one, the initial (previous) state of data would be one non-set dish, and the final (current) two dishes in set.

Key information pieces we needed to extract from data sets were: is current selection a set (should we draw an elongating pill), was previous selection a set (should we draw a contracting pill) or none of the above (only circles should be drawn).

if (isInSet() || (wasInSet() && state.current.size == 1)) {
drawPillAroundSet(canvas, parent)
drawSetLabel(canvas)
} else {
drawCircleAroundEachSingleDish(canvas, parent)
}

Adding padding

As you may have noticed in the “specs”, when dishes do not make a set, the icons should be spread further apart. ItemDecoration makes it a no-brainer thanks to the getItemOffsets() method, which adds spacing around each item. All we had to do was to check if the item in question is not a part of a lunch set:

override fun getItemOffsets(outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State) {
if (isItemViewPartOfSet(parent, view).not())
outRect.right = spacing
}

Final words

It’s somewhat funny that we combined a few of modern Android APIs to create old-fashioned, frame-by-frame animation. The final result is shown below:

Fortunately we could sacrifice more time in favor of a better result when developing aforementioned feature. If you happen to be less lucky, we hope this post will come in handy! Remember to leave a comment in case you have any suggestions!

Don’t forget to tap and hold 👏 and to follow DaftMobile Blog (just press “Follow” button below 😅)! You can also find us on Facebook and Twitter 🙂

--

--