How to implement NestedScrolling on Android?
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.
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.