How to implement NestedScrolling on Android?

AndroidAndroid Appcompat

Android Problem Overview


With support-v4 library 22.1.0 android supports nested scrolling (pre android 5.0). Unfortunately, this feature is not really documented. There are two interfaces (NestedScrollingParent and NestedScrollingChild) as well as two helper delegate classes (NestedScrollingChildHelper and NestedScrollingParentHelper).

Has anyone worked with NestedScrolling on Android?

I tried to setup a little example, where I use NestedScrollView which implements both NestedScrollingParent and NestedScrollingChild.

My layout looks like this:

<android.support.v4.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical">

    <View
        android:id="@+id/header"
        android:layout_width="match_parent" android:layout_height="100dp"
        android:background="#AF1233"/>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/child"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >

      <FrameLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#12AF33"
            android:text="@string/long_text"/>

      </FrameLayout>
    </android.support.v4.widget.NestedScrollView>

  </LinearLayout>

</android.support.v4.widget.NestedScrollView>

I want to display a header view and another NestedScrollView (id = child) in a NestedScrollView (id = parent).

The idea was, to adjust the height of the child scroll view at runtime by using a OnPredrawListener:

public class MainActivity extends Activity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final NestedScrollView parentScroll = (NestedScrollView) findViewById(R.id.parent);
    final NestedScrollView nestedScroll = (NestedScrollView) findViewById(R.id.child);
    parentScroll.setNestedScrollingEnabled(false);
    final View header = findViewById(R.id.header);

    parentScroll.getViewTreeObserver()
        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
          @Override public boolean onPreDraw() {
            if (parentScroll.getHeight() > 0) {
              parentScroll.getViewTreeObserver().removeOnPreDrawListener(this);
              nestedScroll.getLayoutParams().height = parentScroll.getHeight() - 40;
              nestedScroll.setLayoutParams(nestedScroll.getLayoutParams());
              nestedScroll.invalidate();
              return false;
            }
            return true;
          }
        });
    
  }
}

So the header view will be scrolled away partially, 40 pixels will remain visible since I set the height of the nested child scroll view to parentScroll.getHeight() - 40. Alright, setting the height at runtime and scrolling the parent scroll view works like expected (header scrolls out, 40 pixels remain visible and then the child scrollview fills the rest of the screen below the header).

I would expect that "NestedScrolling" means that I can make a scroll gesture anywhere on the screen (touch event caught by parent scroll view) and if the parent scroll view has reached the end the nested child scroll view beginns to scroll. However that seems not to be the case (neither for simple scroll gestures nor for fling gestures).

The touch event is always handled by nested child scrollview if the touch event begins in its boundaries, otherwise by the parent scrollview.

Is that the expected behaviour of "nested scrolling" or is there an option to change that behaviour?

I also tried to replace the nested child scroll view with a NestedRecyclerView. I subclassed RecyclerView and implemented NestedScrollingChild where I delegate all methods to NestedScrollingChildHelper:

public class NestedRecyclerView extends RecyclerView implements NestedScrollingChild {

  private final NestedScrollingChildHelper scrollingChildHelper =
      new NestedScrollingChildHelper(this);


  public void setNestedScrollingEnabled(boolean enabled) {
    scrollingChildHelper.setNestedScrollingEnabled(enabled);
  }

  public boolean isNestedScrollingEnabled() {
    return scrollingChildHelper.isNestedScrollingEnabled();
  }

  public boolean startNestedScroll(int axes) {
    return scrollingChildHelper.startNestedScroll(axes);
  }

  public void stopNestedScroll() {
    scrollingChildHelper.stopNestedScroll();
  }

  public boolean hasNestedScrollingParent() {
    return scrollingChildHelper.hasNestedScrollingParent();
  }

  public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
      int dyUnconsumed, int[] offsetInWindow) {

    return scrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed,
        dyUnconsumed, offsetInWindow);
  }

  public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    return scrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
  }

  public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    return scrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
  }

  public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    return scrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
  }
}

but the NestedRecyclerView doesn't scroll at all. All touch events are caught by the parent scroll view.

Android Solutions


Solution 1 - Android

I spent quite a bit of time on this just going through android code trying to figure out what's going on in NestedScrollView. The following should work.

public abstract class ParentOfNestedScrollView extends NestedScrollView{

    public ParentOfNestedScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /* 
    Have this return the range you want to scroll to until the 
    footer starts scrolling I have it as headerCard.getHeight() 
    on most implementations
    */
    protected abstract int getScrollRange();

    /*
    This has the parent do all the scrolling that happens until 
    you are ready for the child to scroll.
    */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
        if (dy > 0 && getScrollY() < getScrollRange()) {
            int oldScrollY = getScrollY();
            scrollBy(0, dy);
            consumed[1] = getScrollY() - oldScrollY;
        }
    }

    /*
    Sometimes the parent scroll intercepts the event when you don't
    want it to.  This prevents this from ever happening.
    */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
}

Some of my code was borrowed from this question. From this I just extend this class as needed. My xml has the child as a NestedScrollView as a child and the parent as this. This doesn't handle flings as well as I would like, that's a work in progress.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsockeqweView Question on Stackoverflow
Solution 1 - AndroidRyan CView Answer on Stackoverflow