Sample code for Android NestedScrolling nested scrolling

  • 2021-09-11 21:21:50
  • OfStack

Directory 1. What is NestedScrolling?
2. How do I implement NestedScrollingChild?
3. How do I implement NestedScrollingParent?
4. Code Analysis of NestedScrollingChildHelper

1. What is NestedScrolling?

Android introduced the NestedScrolling-nested scrolling mechanism in the Lollipop release. In the event processing mechanism of Android, the event sequence can only be handled by one of the parent View and the child View. In the nested scrolling mechanism, the child View will pass the event to the parent View for processing before processing the event, and the two will cooperate to process the event.

In the nested scrolling mechanism, the parent View needs to implement the NestedScrollingParent interface, and the child View needs to implement the NestedScrollingChild interface. The NestedScrollingChild method has been implemented in View since Lollipop. The nested scrolling process is as follows:

Before scrolling, the child View calls the startNestedScroll method. This method calls the onStartNestedScroll method of the parent View and returns the value of onStartNestedScroll. If true is returned, it indicates that the parent View is willing to receive subsequent scroll events, at which time the parent View's onNestedScrollAccepted will be called. This method 1 is generally called when the child View handles DOWN events. Before the child View scrolls a certain distance, the dispatchNestedPreScroll method is called to pass the scroll distance to the parent View. This method calls back the onNestedPreScroll method of the parent View. If the parent View needs to consume the scroll distance, it only needs to assign the required distance to the parameter consumed of the onNestedPreScroll method. This parameter is an array, with consumed [0] representing the consumed horizontal scroll distance and consumed [1] representing the consumed vertical scroll distance. dispatchNestedPreScroll returns true to indicate that the parent View consumes part or all of the scroll distance. After the child View scrolls a certain distance, the dispatchNestedScroll method is called. If the method returns true, the child View calls the onNestedScroll method of the parent View, passing the consumed and unconsumed scroll distances to the parent View. The dispatchNestedPreFling method is called before the child View handles the Fling event. This method calls onNestedPreFling of the parent View and returns the value of onNestedPreFling. If true, then the parent View processing consumes the Fling event, then the child View should not process the Fling event. If the dispatchNestedPreFling method returns false, the child View calls the dispatchNestedFling method after handling the Fling event, which calls the onNestedFling method of the parent View. The onNestedFling method returns true to indicate that the parent View consumed or handled the Fling event. When the child View stops scrolling, the stopNestedScroll method is called. This method calls the onStopNestedScroll method of the parent View.

Please refer to the official documents for the specific usage of each method mentioned above.

2. How to implement NestedScrollingChild?

Android provides a proxy class NestedScrollingChildHelper for NestedScrollingChild. Therefore, the simplest implementation of NestedScrollingChild is as follows.


public class NestedScrollingChildView extends FrameLayout implements NestedScrollingChild {
 
 private final NestedScrollingChildHelper mChildHelper;

 public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 mChildHelper = new NestedScrollingChildHelper(this);
 }

 @Override
 public void setNestedScrollingEnabled(boolean enabled) {
 mChildHelper.setNestedScrollingEnabled(enabled);
 }

 @Override
 public boolean isNestedScrollingEnabled() {
 return mChildHelper.isNestedScrollingEnabled();
 }

 @Override
 public boolean startNestedScroll(int axes) {
 return mChildHelper.startNestedScroll(axes);
 }

 @Override
 public void stopNestedScroll() {
 mChildHelper.stopNestedScroll();
 }

 @Override
 public boolean hasNestedScrollingParent() {
 return mChildHelper.hasNestedScrollingParent();
 }

 @Override
 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
  int dyUnconsumed, int[] offsetInWindow) {
 return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
  offsetInWindow);
 }

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

 @Override
 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
 return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
 }

 @Override
 public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
 return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
 }
}

Then, call the following method at an appropriate time:
1.startNestedScroll
2.dispatchNestedPreScroll
3.dispatchNestedScroll
4.dispatchNestedPreFling
5.dispatchNestedFling
6.stopNestedScroll

3. How do I implement NestedScrollingParent?

Android provides a proxy class NestedScrollingParentHelper for NestedScrollingParent. The simplest implementation of NestedScrollingParent is as follows.


public class NestedScrollView extends FrameLayout implements NestedScrollingParent {
private final NestedScrollingParentHelper mParentHelper;

 public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 mParentHelper = new NestedScrollingParentHelper(this);
 }

 @Override
 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
 ...
 return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
 }

 @Override
 public void onNestedScrollAccepted(View child, View target, int axes) {
 mParentHelper.onNestedScrollAccepted(child, target, axes);
 ...
 }

 @Override
 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
 ...
 }

 @Override
 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
 ...
 }

 @Override
 public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
 ...
 return false;
 }

 @Override
 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
 ...
 return false;
 }

 @Override
 public void onStopNestedScroll(View child) {
 mParentHelper.onStopNestedScroll(child);
 }

 @Override
 public int getNestedScrollAxes() {
 return mParentHelper.getNestedScrollAxes();
 }
}

4. Code Analysis of NestedScrollingChildHelper


public boolean startNestedScroll(int axes) {
 if (hasNestedScrollingParent()) {
  // Already in progress
  return true;
 }
 if (isNestedScrollingEnabled()) {
  ViewParent p = mView.getParent();
  View child = mView;
  while (p != null) {
  if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
   mNestedScrollingParent = p;
   ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
   return true;
  }
  if (p instanceof View) {
   child = (View) p;
  }
  p = p.getParent();
  }
 }
 return false;
 }

The startNestedScroll method looks up from the NestedScrollingChild for a parent View willing to receive nested scroll events, and if it does, calls the onNestedScrollAccepted method of the parent View. ViewParentCompat is a compatible class of the parent View, which judges the version and calls the methods of View if it is in Lollipop and above. Otherwise, call the interface method of NestedScrollingParent.


public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
 if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
  if (dx != 0 || dy != 0) {
  int startX = 0;
  int startY = 0;
  if (offsetInWindow != null) {
   mView.getLocationInWindow(offsetInWindow);
   startX = offsetInWindow[0];
   startY = offsetInWindow[1];
  }

  if (consumed == null) {
   if (mTempNestedScrollConsumed == null) {
   mTempNestedScrollConsumed = new int[2];
   }
   consumed = mTempNestedScrollConsumed;
  }
  consumed[0] = 0;
  consumed[1] = 0;
  ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

  if (offsetInWindow != null) {
   mView.getLocationInWindow(offsetInWindow);
   offsetInWindow[0] -= startX;
   offsetInWindow[1] -= startY;
  }
  return consumed[0] != 0 || consumed[1] != 0;
  } else if (offsetInWindow != null) {
  offsetInWindow[0] = 0;
  offsetInWindow[1] = 0;
  }
 }
 return false;
 }

Call the onNestedPreScroll method of the parent View and record the scroll offset. Parameter offsetInWindow is a 1-bit group with a length of 2, which records the offset of scrolling and is used to modify the coordinates of Touch events to ensure the accuracy of the next scrolling. The same applies to the dispatchNestedScroll method.

5. Give an example

Implement a simple NestedScrollingParent. The View includes a header View and an RecyclerView. RecyclerView already implements the NestedScrollingChild interface method. When scrolling up, scroll up the head if it is not completely retracted. Scroll RecyclerView if the head is retracted. When scrolling down, if the head is retracted, scroll down the head, otherwise scroll RecyclerView.


public class HeaderLayout extends LinearLayout implements NestedScrollingParent {

 private NestedScrollingParentHelper mParentHelper;

 private int headerH;

 private ScrollerCompat mScroller;

 private boolean resetH = false;

 public HeaderLayout(Context context) {
 this(context, null);
 }

 public HeaderLayout(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public HeaderLayout(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);

 mParentHelper = new NestedScrollingParentHelper(this);
 mScroller = ScrollerCompat.create(this.getContext());
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);

 headerH = getChildAt(0).getMeasuredHeight();
 }

 @Override
 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
 return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
 }

 @Override
 public void onNestedScrollAccepted(View child, View target, int axes) {
 mParentHelper.onNestedScrollAccepted(child, target, axes);
 }

 @Override
 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
 int scrollY = getScrollY();
 if (dy > 0 && scrollY < headerH && scrollY >= 0) {
  int consumedY = Math.min(dy, headerH - scrollY);
  consumed[1] = consumedY;
  scrollBy(0, consumedY);

  if (!resetH) {
  resetH = true;
  int w = getWidth();
  int h = getHeight() + headerH;
  setLayoutParams(new FrameLayout.LayoutParams(w, h));
  }
 } else if (dy < 0 && scrollY == headerH) {
  consumed[1] = dy;
  scrollBy(0, dy);
 } else if (dy < 0 && scrollY < headerH && scrollY > 0) {
  int consumedY = Math.max(dy, -scrollY);
  consumed[1] = consumedY;
  scrollBy(0, consumedY);
 }
 }

 @Override
 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
 }

 @Override
 public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
 int scrollY = getScrollY();
 if (velocityY > 0 && scrollY < headerH && scrollY > 0) {
  if (!mScroller.isFinished()) {
  mScroller.abortAnimation();
  }
  mScroller.fling(0, scrollY, (int)velocityX, (int)velocityY, 0, 0, 0, headerH);
  ViewCompat.postInvalidateOnAnimation(this);
  return true;
 }
 return false;
 }

 @Override
 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
 return false;
 }

 @Override
 public void onStopNestedScroll(View child) {
 mParentHelper.onStopNestedScroll(child);
 }

 @Override
 public int getNestedScrollAxes() {
 return mParentHelper.getNestedScrollAxes();
 }

 @Override
 public void computeScroll() {
 if (mScroller.computeScrollOffset()) {
  scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
  postInvalidate();
 }
 }
}


Related articles: