- Animating a Scroll Gesture
- This lesson teaches you to
- You should also read
- Try it out
- Understand Scrolling Terminology
- Implement Touch-Based Scrolling
- Complex UI/Animations on Android
- Before we begin
- Complex UI/Animations on Android — featuring MotionLayout
- Exploring complex multi-step animations with MotionLayout (and Coroutines).
- Let’s get into it..
- 1. Toolbar Animation when scrolling
- 2. RecyclerView Item Expand/Collapse Animation
- 3. Tabs Scrolling Animation
- #1 Scrolling the Tabs RecyclerView as the ViewPager is scrolled:
- #2 Transforming the current RecyclerView Item
- 4. Filter Sheet Open/Close Animation
- #1 FAB Arc Path Animation
- #2 Scale Down Animation
- #3 Reveal Animation
- #4 Settle Animation
- Choroeographing animation with AnimatorSet
- Filtering Animation
- Clearing Filters Animation
- Conclusion
Animating a Scroll Gesture
This lesson teaches you to
You should also read
- Input Events API Guide
- Sensors Overview
- Making the View Interactive
- Design Guide for Gestures
- Design Guide for Touch Feedback
Try it out
In Android, scrolling is typically achieved by using the ScrollView class. Any standard layout that might extend beyond the bounds of its container should be nested in a ScrollView to provide a scrollable view that’s managed by the framework. Implementing a custom scroller should only be necessary for special scenarios. This lesson describes such a scenario: displaying a scrolling effect in response to touch gestures using scrollers.
You can use scrollers ( Scroller or OverScroller ) to collect the data you need to produce a scrolling animation in response to a touch event. They are similar, but OverScroller includes methods for indicating to users that they’ve reached the content edges after a pan or fling gesture. The InteractiveChart sample uses the EdgeEffect class (actually the EdgeEffectCompat class) to display a «glow» effect when users reach the content edges.
Note: We recommend that you use OverScroller rather than Scroller for scrolling animations. OverScroller provides the best backward compatibility with older devices.
Also note that you generally only need to use scrollers when implementing scrolling yourself. ScrollView and HorizontalScrollView do all of this for you if you nest your layout within them.
A scroller is used to animate scrolling over time, using platform-standard scrolling physics (friction, velocity, etc.). The scroller itself doesn’t actually draw anything. Scrollers track scroll offsets for you over time, but they don’t automatically apply those positions to your view. It’s your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.
Understand Scrolling Terminology
«Scrolling» is a word that can take on different meanings in Android, depending on the context.
Scrolling is the general process of moving the viewport (that is, the ‘window’ of content you’re looking at). When scrolling is in both the x and y axes, it’s called panning. The sample application provided with this class, InteractiveChart , illustrates two different types of scrolling, dragging and flinging:
- Dragging is the type of scrolling that occurs when a user drags her finger across the touch screen. Simple dragging is often implemented by overriding onScroll() in GestureDetector.OnGestureListener . For more discussion of dragging, see Dragging and Scaling.
- Flinging is the type of scrolling that occurs when a user drags and lifts her finger quickly. After the user lifts her finger, you generally want to keep scrolling (moving the viewport), but decelerate until the viewport stops moving. Flinging can be implemented by overriding onFling() in GestureDetector.OnGestureListener , and by using a scroller object. This is the use case that is the topic of this lesson.
It’s common to use scroller objects in conjunction with a fling gesture, but they can be used in pretty much any context where you want the UI to display scrolling in response to a touch event. For example, you could override onTouchEvent() to process touch events directly, and produce a scrolling effect or a «snapping to page» animation in response to those touch events.
Implement Touch-Based Scrolling
This section describes how to use a scroller. The snippet shown below comes from the InteractiveChart sample provided with this class. It uses a GestureDetector , and overrides the GestureDetector.SimpleOnGestureListener method onFling() . It uses OverScroller to track the fling gesture. If the user reaches the content edges after the fling gesture, the app displays a «glow» effect.
Note: The InteractiveChart sample app displays a chart that you can zoom, pan, scroll, and so on. In the following snippet, mContentRect represents the rectangle coordinates within the view that the chart will be drawn into. At any given time, a subset of the total chart domain and range are drawn into this rectangular area. mCurrentViewport represents the portion of the chart that is currently visible in the screen. Because pixel offsets are generally treated as integers, mContentRect is of the type Rect . Because the graph domain and range are decimal/float values, mCurrentViewport is of the type RectF .
The first part of the snippet shows the implementation of onFling() :
When onFling() calls postInvalidateOnAnimation() , it triggers computeScroll() to update the values for x and y. This is typically be done when a view child is animating a scroll using a scroller object, as in this example.
Most views pass the scroller object’s x and y position directly to scrollTo() . The following implementation of computeScroll() takes a different approach—it calls computeScrollOffset() to get the current location of x and y. When the criteria for displaying an overscroll «glow» edge effect are met (the display is zoomed in, x or y is out of bounds, and the app isn’t already showing an overscroll), the code sets up the overscroll glow effect and calls postInvalidateOnAnimation() to trigger an invalidate on the view:
Here is the section of the code that performs the actual zoom:
This is the computeScrollSurfaceSize() method that’s called in the above snippet. It computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible, this is simply the current size of mContentRect . If the chart is zoomed in 200% in both directions, the returned size will be twice as large horizontally and vertically.
For another example of scroller usage, see the source code for the ViewPager class. It scrolls in response to flings, and uses scrolling to implement the «snapping to page» animation.
Источник
Complex UI/Animations on Android
Before we begin
- UX/UI Credit goes to Yaroslav Zubko. You can find the design here.
- TLDR? View thecode on Github or get the app on thePlayStore .
- 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.
Complex UI/Animations on Android — featuring MotionLayout
Exploring complex multi-step animations with MotionLayout (and Coroutines).
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:
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.
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 call requestLayout() .
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 the RecyclerView OnScrollListener , the transformation won’t work when the RecyclerView is scrolled by the user. Ideally, we can solve this by doing the transformation caluclation in the OnScrollListener 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 custom RecyclerView and LayoutManager :
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.
#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*).
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:
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:
#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.
Источник