Carry on Scrolling

Carry on Scrolling

If you’ve used the Material Design Components library (formerly known as the design support library), and specifically its scrolling components such as AppBarLayout, you probably noticed that flinging can sometimes stop quite abruptly. You can see a quick animation comparing the issue and new fixed version above.

As you can see, the scrolling gesture is seamless while the user scrolls up, but on v25.x, the fluidity breaks when the user flings down. What you really want is for the app bar to get dragged down too as you can see in v26.x.

The main reason for this is due to limitations in a set of Android APIs called nested scrolling. They were added in Android Lollipop (API 21) to enable the sort of scrolling gestures which Material Design formalizes on its ‘Scrolling techniques’ patterns page. The primary goal of the APIs is to allow listening & interception of scrolling events by any of its parent views.

This has now been augmented in 26.0.0-beta2 of the support libraries with some improvements to the nested scrolling APIs. So if you’re just interested in the behavior, please go update and give it a try!

The rest of this post is going to be a deep-dive on how we actually implemented it.


Nested scrolling — a recap

So let’s quickly cover the current nested scrolling APIs. There are two main parts to the API, scrolling and flinging, but it’s easiest to think about the API from the timeline of a scroll gesture. From here on, whenever I refer to view, I’m talking about the scrolling view which the user is touching (i.e. ScrollView, RecyclerView, etc).

👇 User touches screen. On the ACTION_DOWN event, the view will call startNestedScroll() on each of its parents until one returns true. Returning true means that the parent is interested in this scroll gesture. If none of the parents return true, the nested scroll is cancelled and the view goes about its normal business. For the rest of this post we’ll assume that a parent has returned true.

👋 User moves finger. On each and every ACTION_MOVE event, the view will call dispatchNestedPreScroll() to send events to the parent allowing it to consume some/all/none of the distance travelled by the user’s finger. If the parent does not consume all of the movement, the view then consumes the remainder itself (by moving position, etc) and sends a dispatchNestedScroll() event with the value which it consumed.

👆 User lifts finger. On the ACTION_UP event, the view will calculate whether it needs to continue the movement by flinging itself. If it deems that there is enough velocity left in the gesture to continue, it will call dispatchNestedPreFling() to allow the parent to consume the velocity. If the parent returns true and consumes it, the view has then finished its work. Otherwise the view will start a fling and then immediately call dispatchNestedFling(). The view will immediately call stopNestedScroll() marking the nested scroll as finished, even though the view may actually be flinging itself.

That last sentence is the crux of the issue here. The parent probably does not want to consume the entire fling gesture, it likely just wants to react to the fling just like it reacts to a scroll. Unfortunately that isn’t supported by the current API though (as of v25.3.1).

Nested Scrolling++

In 26.0.0-beta2 of the support libraries, we have released some improvements to the nested scrolling APIs, to help fix that very issue.

If you look at the new API, all we’ve actually done is added a new parameter called type to the existing methods. The type passed to the method tells you what kind of input is driving that specific scroll event, and is one of two options currently: ViewCompat.TYPE_TOUCH and ViewCompat.TYPE_NON_TOUCH.

TYPE_TOUCH is passed when the event is directly triggered from the user touching the screen. TYPE_NON_TOUCH is triggered when the user is not touching the screen. Currently this is only used in the fling scenario but it might be utilized in other areas such as keyboard navigation in the future.

To maintain API compatibility, we assume TYPE_TOUCH when not specified. For example, here’s the startNestedScroll() declarations:

@Override
public void startNestedScroll(int axes) {
    startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
}

@Override
public void startNestedScroll(int axes, int type) {
    // Do something
}

In practice

So, it’s great that we have these new methods but how do you actually use them in practice? Well for the most part you probably won’t need to look at the type parameter at all. You should just process each event the same regardless of how it was triggered.

So why have we even added the parameter? Remember the original issue? To fix that we’ve had to change some of the characteristics of how the API is called from the scrolling view, specifically around flinging.

Now let’s step through the same scenario as before:

👇 User touches screen. Exactly same as above, but this time the TYPE_TOUCH parameter is passed through to startNestedScroll(TYPE_TOUCH).

👋 User moves finger. Ditto, same story.

👆 User lifts finger. Ditto. stopNestedScroll(TYPE_TOUCH) is called and ‘touch’ nested scrolling has finished.

🎢 View starts flinging. Finally, the interesting bit! If the view starts flinging itself, we will now start a whole new round of nested scrolling, but this time with the TYPE_NON_TOUCH type. That means: startNestedScroll(TYPE_NON_TOUCH), followed by dispatchNestedPreScroll(TYPE_NON_TOUCH) + dispatchNestedScroll(TYPE_NON_TOUCH), with a final touch of stopNestedScroll(TYPE_NON_TOUCH). This time everything is driven from the view’s fling (usually a Scroller) rather than touch events.

That’s great, but earlier you told me to ignore the type parameter!?!

Indeed I did. The type parameter has been mostly added to maintain the current API and its runtime characteristics. For 99% of usage, you should treat both types of events exactly the same, and that’s exactly what AppBarLayout does by ignoring the type.

How do I use it?

The new APIs and functionality are for the moment a support-library only feature.

If you are just using AppBarLayout and friends, and not explicitly using the nested scrolling APIs, you can just update to 26.0.0-beta2 and enjoy.

However if you are explicitly implementing the nested scrolling APIs, you will need to make some changes to benefit*. The new methods have been added to NestedScrollingChild2 and NestedScrollingParent2, so just switch the appropriate interface to implement. They have also been added to CoordinatorLayout.Behavior so you should be able to just add , int type to the end to any of your relevant method declarations and it will just work™.

* To be clear: if you make no changes, your app’s behavior should not change. If you see differently, please raise a bug.

Final result

Just because, here’s another video showing a flinger slightly slower: