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.

demo3Source

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à

demo

Full source code can be found here: https://github.com/ggajews/nestedscrollingchildviewdemo. If you still want to make ListView work with the CoordinatorLayout you need to implement your own NestedScrollingChild ListView. Now you know how to do it.

Posted by

Grzesiek Gajewski

Share this article

  • danilao

    I’m using your example with a CollapsingToolbarLayout and once I scroll down in my child view to expand the toolbar completely if I keep scrolling, the entire CollapsingToolbarLayout view keep moving down. Have you tested some similar?

    • Grzegorz Gajewski

      I didn’t notice anything alike. Can you post your layout?

  • Sarath Babu

    I tried you example in Git, and I modified the compile version to 23 and support library to 23.1.1 then, with width and height “match_parent” works fine but if I change the height to lets say “300dp” the scrolling doesn’t work.

    • Grzegorz Gajewski

      Hey @disqus_6yAGE9fF1w:disqus. Sorry for late reply. If you still have this problem can you post link to your git project?

      • Sarath Babu

        Hi, this git repo https://github.com/SarathBabu/NestedScrollingChildViewBaseLab, Now it is working when placed it in a RelativeLayout. Can you please look into the code and explain why it is working ?

        • Grzegorz Gajewski

          It actually works, but when you set it to 300dp the view is positioned in the top (black square on the screenshot)

  • Wolfgang Sieber

    i use a hotizontal recyclerview in my activity (same you can see on playstore start-activity), but its very difficult to scroll horizontal the recyclerview! i am also looking for a result for snapping recyclerview items on start!

  • Palak Darji

    I tried to do same with ListView.. Its works but It flickers too much. Any solution??