
Complex UI/Animations on Android
Before we begin
- UX/UI Credit goes to Yaroslav Zubko. You can find the design here.
- TLDR? View the code on Github or get the app on the PlayStore.
- All parts of the animations were done via code (Kotlin). No XML animations/transitions were used because it would split up the animation code all over the place.
- This article doesn’t use
MotionLayout
because I wanted to try and achieve this by using Android animation fundamentals. The next article talks about doing the same by only using MotionLayout. Reading both offers a comprehensive comparison between both the methods.
Let’s get into it..
For most animations, I tend to use ValueAnimator.ofFloat()
that animates from 0f to 1f (forward) / 1f to 0f (reverse). We can animate everything based on the progress between 0f and 1f. Here’s a simple top-level function that I use to make things easier:
1. Toolbar Animation when scrolling

This toolbar animation was done using a custom CoordinatorLayout Behavior
. There are many resources on how this can be done. Here’s the shortened code
The important part is the onNestedScroll
method where we use dyConsumed
and dYUnconsumed
to shrink and expand the toolbar accordingly.
This is how we set it to the appbar view in the activity:
(appbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = ToolbarBehavior()
2. RecyclerView Item Expand/Collapse Animation

To perform this animation, we need to calculate the original and expanded height. Since the view has it’s height set to wrap_content
, we need to calculate the heights programatically (in onBindViewHolder
). Usually, the height of a view with wrap_content is set after the view has been laid out. We can use these extensions from the ktx library to help us.
View.doOnLayout{view -> /* ... */}
View.doOnNextLayout{view -> /* ... */}
By using that, we can get originalHeight
. For expandedHeight
, we need to immediately make expandView
(ViewGroup that contains all the expanded views) visible, measure the expandedHeight
on the next layout and hide it again. All this happens in one layout pass, so it’s practically invisible to the user’s eyes (If the heights are hardcoded values, then this step can be skipped).
Note: After writing this article, I realised doOnPreDraw{..}
is better for this scenario. Read this comment to know more.
Once we have the heights and the widths, animating the view is simple. Here’s a sample of the expandItem()
method from the RecyclerView Adapter. Calling this when an item is clicked, expands/collapses the view based on the expand
variable.
When animating the width/height of a view using
layoutParams
, don’t forget to callrequestLayout()
.
Using a variable like expandedModel
to track which item is expanded, we can decide whether to collapse/expand the item. But how do we simultaneously expand one item and collapse another? (Look at animation above).
We can use recyclerView.findViewHolderForAdapterPosition()
to get the expanded viewholder and collapse it. At the same time, we can expand the clicked item.
3. Tabs Scrolling Animation

The tabs section is a RecyclerView
that is in sync with the ViewPager2
below it. It also has a transformation applied to the active item similar to a ViewPager transformation. There are 2 things to consider:
#1 Scrolling the Tabs RecyclerView as the ViewPager is scrolled:
ViewPager2's onPageScrolled
callback has position
and positionOffset
variables which determine the absolute scroll position of the ViewPager. Unfortunately, RecyclerView does not deal with absolute values since it recycles views and doesn’t know about off-screen views. It doesn’t implement the absolute scrollTo(x,y)
method but has the relative scrollBy(x,y)
method.
Since the number of tabs and the width of each tab item are fixed here, we use totalTabsScroll
to keep track of the absolute total scroll position using RecyclerView’s OnScrollListener()
callback. This allows us to convert absolute values to relative values and use scrollBy()
.
#2 Transforming the current RecyclerView Item
To transform the current item (scale up and fade color), we need absolute values. Since the ViewPager2
callback already gives us these, we can directly create a pseudo-transformer for the RecyclerView here.
There are a couple of minor issues with this approach:
- Since the transformation happens from the
ViewPager2
callback and not theRecyclerView OnScrollListener
, the transformation won’t work when theRecyclerView
is scrolled by the user. Ideally, we can solve this by doing the transformation caluclation in theOnScrollListener
but I decided to disable scrolling altogether and only enable clicks. - Disabling scrolling in the LayoutManager is not good because it stops programatic scrolling using
scrollBy()
as well. We can solve this by using a customRecyclerView
andLayoutManager
:
4. Filter Sheet Open/Close Animation

This animation consists of 4 sections:
- Floating Action Button (FAB) arc path animation where the button moves to the centre of the screen.
- Scale Down Animation which shrinks and fades all the RecyclerView items in the background.
- FAB Reveal Animation where the button circular reveals into a bottom sheet while the filter icon moves down.
- Elements Settle Animation where the tabs slide up, viewpager fades in and the bottom bar slides and fades in.
The FAB is actually just a CardView
. For a lot of the animations the CardViewAnimatorHelper
helper class is used. It provides a ValueAnimator making it easy to animate CardView attributes like elevation, radius, etc.
#1 FAB Arc Path Animation

CardViewAnimatorHelper
is all we need for this animation. Supplying isArcPath = true
takes care of arcing the path. Internally it uses ArcAnimator to get arc path values.
val pathAnimator = CardViewAnimatorHelper(
cardView = fab,
startX = fabX, startY = fabY,
endX = fabX2, endY = fabY2,
startElevation = fabElevation, endElevation = fabElevation2,
isArcPath = true,
duration = pathAnimDuration,
interpolator = pathAnimInterpolator
).getAnimator(isOpening)
#2 Scale Down Animation

All the RecyclerView
items fade and scale down in the background as the arc path happens. To do this, we need to animate all the visible items in the LayoutManager
. In the RecyclerView Adapter:
#3 Reveal Animation

This reveal animation does not use the Android Utils Circular Reveal because the filter icon needs to translate down simultaneously. We can do this by setting layout_gravity = bottom|center_horizontal
to the icon in the CardView and calling requestLayout()
when animating the CardView. Also, you may have noticed, the reveal is not a perfect circular reveal. It’s an increase in circle size of the CardView followed by un-curving the corners (radius = 0).
CardViewAnimatorHelper
can fetch you an animator using getAnimator()
directly or you can set progress
to it for manual control. We use the latter method here.
#4 Settle Animation

This animation is nothing special. It’s just views fading, sliding and settling in. But what’s interesting is, although it may seem fluid coming off of the reveal animation, it isn’t.
The reveal animation was done on the fab CardView
. But once the reveal is done, another ViewGroup main_container
which has a higher elevation than the fab is made visible and the settle animation happens on the elements in that ViewGroup.
After the reveal animation, main_container
is made visible and picks up the animation from there. The reverse is done when closing the filter sheet. This was done because if all the elements were in the fab CardView
, it’s not easy to animate them considering how the fab grows and shrinks in size (*cough* MotionLayout
*cough*).
if (isOpening) revealAnimator.doOnEnd { mainContainer.isVisible = true }
else revealAnimator.doOnStart { mainContainer.isVisible = false }
Note that the bottom bar is a separate CardView. The fab and the bottom bar being separate CardViews helps with the next animation (explained soon).
Choroeographing animation with AnimatorSet
I’m not too fond of the AnimatorSet API because of some of it’s gotchas and other reasons. (Check out Chris Banes’s post on using coroutines with animators). But here’s how it’s done for this animation:
val set = AnimatorSet()
if (isOpening) {
set.play(pathAnimator).with(scaleDownAnimator)
set.play(pathAnimator).before(revealAnimator)
set.play(revealAnimator).before(settleAnimator)
} else {
set.play(settleAnimator).before(revealAnimator)
set.play(revealAnimator).before(pathAnimator)
set.play(pathAnimator).with(scaleDownAnimator)
}
set.start()
Filtering Animation
This animation consists of 6 animations. We will not be going into too much detail for this one as these animations mostly use CardViewHelperAnimator
, CircleCardViewHelperAnimator
and other concepts explained before. The source code is well documented for every step.

- Fab Collapse Animation. The fab collapses to it’s original size.
- Bottom Bar Animation. The bottom bar translates and collapses into the fab while centering the close icon and fading out the filter icon.
- Tabs and Viewpager fade out animation.
- Close Icon rotate animation to simulate loading.
- Fab Arc Path Animation
- RecyclerView items scale down animation.
As mentioned before, having the fab, filter sheet and bottom bar as separate views helps us achieve this animation because each of them can be controlled separately. The bottom bar is a separate cardview for the sole purpose of being able to animate corner radius easily to turn into a circle.
#1 Fab Collapse Animation grows the fab slightly before collapsing. This is done using an AnticipateInterpolator
. The animation itself is done using CircleCardViewHelperAnimator
.
#2 Bottom Bar Animation turns the bottom bar into a small circle, inset into the fab. This is also done using the helper class and supplying appropriate values. Additionaly, the close icon is translated to the middle of the circle and the filter icon is faded out.
#3 Tabs and ViewPager fade out (alpha and scale). Nothing special.
The first 3 animations are done in parallel. At the end of it, main_container
(containing filter sheet and bottom bar) is hidden and the rest of the animation takes place on the fab.
Note: The fab has a close icon that’s identical to the bottom bar inset end result so it’s seamless when transitioning.
#4 Close Icon is rotated on the fab while the RecyclerView in the backgroud is filtered. RecyclerView Item Animation duration can be adjusted:
recyclerView.itemAnimator?.removeDuration = duration
recyclerView.itemAnimator?.addDuration = duration
#5 Fab Arc Path and #6 RecyclerView scale down animations are explained earlier. It’s the exact same.
AnimatorSet is used to choreograph the animation in this order:(1,2,3) together, then 4, then (5, 6) together
Clearing Filters Animation

This is the last animation. It clears all the filters while animating the fab. This involves concepts already explained before so we will not be breaking this down.
Conclusion
A lot of these animations may not be practical but this post was written mainly because a lot of the concepts here are quite useful.
MotionLayout makes a lot of this much easier. If you want to know more, read my next article on how I achieved all the same animations here by only using MotionLayout.
Hope you enjoyed my first post here, on Medium 😃. Check out the source code if you’re further interested!