In my previous post I described CoordinatorLayout and how to write custom behaviour for views. There were a few comments asking how to write custom scrolling view that plays well with CoordinatorLayout and AppBarLayout . Some readers were also curious why AppBarLayout is working with RecyclerView and not with ListView .

Let’s take a brief look at the magic that shows/hides AppBarLayout on scrolls coming from a RecyclerView .

Source

Inspecting the source code of AppBarLayout we can see that the default behavior annotation points to @DefaultBehavior(AppBarLayout.Behavior.class)

The AppBarLayout.Behavior implements methods that react to the scroll events:

void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target)

This explains why AppBarLayout is changing its location when placed inside a CoordinatorLayout . Now the question is how CoordinatorLayout gets notified by its child when scrolling occurs. If we take a look at RecyclerView class signature we might find our answer.

Say hello to NestedScrollingChild and NestedScrollingParent interfaces.

We can see that RecyclerView implements NestedScrollingChild interface. Let’s see what documentation says about it:

This interface should be implemented by View subclasses that wish to support dispatching nested scrolling operations to a cooperating parent ViewGroup.

To receive dispatched nested scrolling operations CoordinatorLayout needs to implement NestedScrollingParent interface.

This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view.

Let’s dive into NestedScrollingChild and NestedScrollingParent communication process.

When the child is about to begin scroll (that corresponds to onTouch action down event) it calls onScrollStarted method. With each scroll step child is calling two methods dispatchPreScroll and dispatchScroll . First method offers an opportunity for the parent view to consume some or all of the scroll operation before the child view consumes it. If the parent returns true the child should reduce its scroll by the value consumed by the parent. Method dispatchNestedScroll is called to report scroll progress to the parent. It contains both the consumed and the unconsumed scroll values. The parent may treat these values differently:

An implementation may choose to use the consumed portion to match or chase scroll position of multiple child elements, for example. The unconsumed portion may be used to allow continuous dragging of multiple scrolling or draggable elements, such as scrolling a list within a vertical drawer where the drawer begins dragging once the edge of inner scrolling content is reached.

CoordinatorLayout acts as a proxy. It receives callbacks from the child and forwards it to its children behavior classes.

When scroll process finishes the child calls onScrollStoped callback.

Implementing custom NestedScrollingChild view.

Now that we know how it works, we can implement a sample application with a custom view that detects scroll and delegates it to CoordinatorLayout .

Let’s start with the View

public class NestedScrollingChildView extends View implements NestedScrollingChild, OnGestureListener

Good guys from the Android Team wanted to make the task a little bit easier for us and introduced the NestedScrollingChildHelper .

Helper class for implementing nested scrolling child views compatible with Android platform versions earlier than Android 5.0 Lollipop (API 21).

All we need to do is create helper instance and delegate appropriate methods.

By default nested scroll is disabled. We can enable it in the constructor.

setNestedScrollingEnabled(true);

Now we need to detect the scroll. Let’s use an instance of GestureDetectorCompat to reduce boilerplate.

@Override public boolean onTouchEvent(MotionEvent event){ return mDetector.onTouchEvent(event); }

In the onDown method we should call onStartScrolling and pass vertical axis flag as parameter.

@Override public boolean onDown(MotionEvent e) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); return true; }

From the onScroll method we call dispatchNestedPreScroll with calculated distanceY followed by the dispatchNestedScroll call. Since our view doesn’t scroll at all let’s pass 0 as the consumed and unconsumed scroll value.

@Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { dispatchNestedPreScroll(0, (int) distanceY, null, null); dispatchNestedScroll(0, 0, 0, 0, null); return true; }

The last part is to call stopNestedScroll when the scroll process is over. Since GestureDetectorCompat doesn’t provide appropriate callback we need to do it in a custom implementation of our view’s onTouchEvent method.

@Override public boolean onTouchEvent(MotionEvent event){ final boolean handled = mDetector.onTouchEvent(event); if (!handled && event.getAction() == MotionEvent.ACTION_UP) { stopNestedScroll(); } return true;

}

For the simplicity we won’t handle the fling in this example.

And voilà