The use of VerticalNestedScrollLayout

Introduction to the

VerticalNestedScrollLayout realize the common component of nested vertical scrolling. There are only two direct child views inside: the head and the body.

Two child View are written in the layout, as follows: there are two immediate child VerticalNestedScrollLayout View, NestedScrollViewh and FrameLayout.

<com.kaola.base.ui.scroll.VerticalNestedScrollLayout
        xmlns:vnsl="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        vnsl:isScrollDownWhenFirstItemIsTop="true"
        >

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


        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >...<android.support.v7.widget.RecyclerView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:overScrollMode="never"
                />...</FrameLayout>
    </com.kaola.base.ui.scroll.VerticalNestedScrollLayout>
Copy the code

VerticalNestedScrollLayout as father of nested rolling components, need to cooperate with support for nested scrolling the View components.

Layout introduction:

  1. The first child View is NestedScrollViewh, which is a component of the system that implements nested scrolling. Essentially, it inherits FrameLayout and implements NestedScrollingParent and NestedScrollingChild interfaces. So NestedScrollViewh can be either a parent or a child.
  2. The second child View is FrameLayout, which does not support nested scrolling, but the child View of FrameLayout has RecyclerView, and RecyclerView implements NestedScrollingChild. Nested scrolling does not require a direct child View or a parent View to support nested scrolling, but can also be indirect, with internal traversal lookup logic.

VerticalNestedScrollLayout support properties and interface callback:

  1. IsScrollDownWhenFirstItemIsTop when slipping, whether only when the main body top, head down
  2. IsAutoScroll header whether to automatically scroll to the top or bottom
  3. HeaderRetainHeight Specifies the reserved height of the head. This is often used for example, if there is a SmartTabLayout at the bottom of the head layout, set headerRetainHeight to the height of the SmartTabLayout to make it stick to the top.
  4. VerticalNestedScrollLayout also support the rolling in, scroll to the top and bottom of the callback.
public interface OnScrollYListener {
        void onScrolling(int scrollY, boolean isTop);

        void onScrollToTop();

        void onScrollToBottom();
    }
Copy the code

Common problems:

  1. The first subview uses a normal ViewGroup (such as the LinearLayout), so the head cannot slide, only the lower body. In this case, you need to use NestedScrollView instead of the normal ViewGroup
  2. There is a RecyclerView supporting horizontal sliding in the first sub-view, horizontal sliding and vertical sliding nesting scrolling, resulting in horizontal sliding can also be vertical sliding, at this time need to disable horizontal sliding nesting scrolling of RecyclerView.

mRecyclerView.setNestedScrollingEnabled(false);

VerticalNestedScrollLayout principle

VerticalNestedScrollLayout is inherited nested LinearLayout implementation NestedScrollingParent father rolling components, set its direction in initFromAttributes method for vertical, and obtain the properties in the layout. The three properties can also be set using the set… method

private void initFromAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
    setOrientation(LinearLayout.VERTICAL);
    mParentHelper = new NestedScrollingParentHelper(this);

    TypedArray a = context.obtainStyledAttributes(attrs, com.kaola.base.R.styleable.VerticalNestedScrollLayout,
            defStyleAttr, 0);
    mIsScrollDownWhenFirstItemIsTop =
            a.getBoolean(R.styleable.VerticalNestedScrollLayout_isScrollDownWhenFirstItemIsTop, false);
    mIsAutoScroll = a.getBoolean(R.styleable.VerticalNestedScrollLayout_isAutoScroll, false);
    mHeaderRetainHeight = (int) a.getDimension(R.styleable.VerticalNestedScrollLayout_headerRetainHeight, 0);
    a.recycle();
}
Copy the code

Retrieve the head (mHeaderView) and body (mBodyView) using the onFinishInflate method

@Override
protected void onFinishInflate(a) {
    super.onFinishInflate();
    mHeaderView = getChildAt(0);
    mBodyView = getChildAt(1);
}
Copy the code

And the addView method limits adding views.

@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (getChildCount() > 1) {
        throw new IllegalStateException("VerticalNestedScrollLayout can host only two direct child");
    }
    super.addView(child, index, params);
}
Copy the code

Then there are more important measurement methods, mainly including the following steps:

  1. Measure head height
  2. Gets the maximum scrolling distance in preparation for automatic head scrolling
  3. Measure the height of the subject
  4. The height of the set VerticalNestedScrollLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // If the mHeaderView height is greater than the screen height, only the screen height will be displayed
    mHeaderView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    // Maximum rolling distance: head minus reserved height
    mMaxScrollHeight = mHeaderView.getMeasuredHeight() - mHeaderRetainHeight;
    // Set the height of the body: match_parent is set in the code
    if (mBodyView.getLayoutParams().height < getMeasuredHeight() - mHeaderRetainHeight) {
        mBodyView.getLayoutParams().height = getMeasuredHeight() - mHeaderRetainHeight;
    }
    // Set your own height
    setMeasuredDimension(getMeasuredWidth(), mBodyView.getLayoutParams().height + mHeaderView.getMeasuredHeight());
}
Copy the code

Red box said screen, measure the height of the VerticalNestedScrollLayout is actually getting high, if no measurement is nested rolling, sliding upwards, will appear at the bottom of the blank area

The onNestedPreScroll and onNestedPreFling methods are implemented in the NestedScrollingParent interface.

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

    if (canScroll(target, dy)) {
        scrollBy(0, dy);
        consumed[1] = dy; ......}}Copy the code

This method is called before the child View starts to scroll, which is called before the child View rolls the parent View first. Here we need to determine whether the parent View should scroll. In the code hiddenTop is the action of hiding the head, showTop is the action of showing the head, satisfy either, need to scroll the parent View. The code is as follows:

private boolean canScroll(View target, int dy) {
    boolean hiddenTop = dy > 0 && getScrollY() < mMaxScrollHeight;
    boolean showTop = dy < 0 && getScrollY() > 0;
    if(mIsScrollDownWhenFirstItemIsTop) { showTop = showTop && ! target.canScrollVertically(-1);
    }
    return hiddenTop || showTop;
}
Copy the code

If consumed[1] = dy; So the parent View consumes all the vertical sliding distance, if consumed[1] = dy * 0.5f; The parent View consumes half, so the user sees the head and body scrolling at the same time.

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    if (mIsScrollDownWhenFirstItemIsTop && target.canScrollVertically(-1)) {
        return false;
    }

    if(mScrollAnimator ! =null && mScrollAnimator.isStarted()) {
        mScrollAnimator.cancel();
    }
    mIsFling = true;
    if (velocityX == 0&& velocityY ! =0) {
        if (velocityY < 0) {
            autoDownScroll();
        } else {
            autoUpScroll();
        }
        if (mIsScrollDownWhenFirstItemIsTop) {
            return true; }}return false;
}
Copy the code

Without this section, the head looks inert and the health of the user is poor.

The method canScrollVertically(-1) checks whether Target can pull down. There is no such as RecyclerView placed at the top, you can pull down, mRecyclerView. CanScrollVertically (1) returns true

Then use velocityY to determine whether to automatically roll to the top or bottom; Returning true means that the parent View consumes the Fling event; false does not.

Nested rolling principle chapter internal implementation

The two interfaces in nested scrolling were mentioned above. The methods in the NestedScrollingParent and NestedScrollingChild interfaces are as follows:

NestedScrollingChild

  • StartNestedScroll: start scroll method to find the external control that receives sliding distance information.
  • DispatchNestedPreScroll: Dispatches slide information to the outer control before the inner control processes the slide.
  • DispatchNestedScroll:, after processing the sliding control the rest of the sliding distance information distributed to outside control.
  • StopNestedScroll: the stopNestedScroll method is used to clear the related states of nested slides
  • SetNestedScrollingEnabled and isNestedScrollingEnabled: a pair of get&set method, used to determine whether the control support nested sliding.
  • DispatchNestedPreFling and dispatchNestedFling are similar to Scroll.

NestedScrollingParent

  • OnStartNestedScroll: corresponding to startNestedScroll, the internal controller calls this method of the external control to determine whether the external control receives sliding information.
  • OnNestedScrollAccepted: This method is called back when the outer control determines that it has received a slide message, allowing the outer control to do some upfront work on nested slides.
  • OnNestedPreScroll: The key method, receives the sliding distance information of the internal controller before sliding, in which the external control can preferentially respond to the sliding operation, consuming part or all of the sliding distance.
  • OnNestedScroll: the key method to receive the sliding distance information of the internal controller after sliding processing, where the external control can choose whether to process the remaining sliding distance.
  • OnStopNestedScroll: corresponds to stopNestedScroll to do some finishing work.
  • GetNestedScrollAxes: returns the direction of nested sliding, differentiating horizontal sliding from vertical sliding
  • OnNestedPreFling and onNestedFling: Same as above

The process of nested scrolling:

After receiving the scroll event, the child view initiates nested scroll, and asks the parent view whether to scroll first. After the parent view processes its own scrolling needs, it returns to the child view to process its own scrolling needs. If the parent view consumes some scrolling distance, the child view can only obtain the remaining scrolling distance for processing. The child View handles its scrolling needs and then goes back to the parent View and handles the rest of the scrolling distance. Inertia fling.

The above process with source code to explain (child View for RecyclerView, parent View inherited NestedScrollingParent View) roughly as follows:

NestedScrollingChild’s startNestedScroll is used to initiate nested scrolling. When onInterceptTouchEvent and onTouchEvent action == motionEvent. ACTION_DOWN, ignore onInterceptTouchEvent and look directly at onTouchEvent.

See RecyclerView startNestedScroll, found that is adjustable in NestedScrollingChildHelper startNestedScroll method, check startNestedScroll, found a traversal process, Find the parent View whose onStartNestedScroll returns true and execute onNestedScrollAccepted to stop traversal. The order of nested scroll execution so far is as follows:

StartNestedScroll → Parent onStartNestedScroll → Parent onNestedScrollAccepted

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

Copy the code

Then dispatchNestedPreScroll and scrollByInternal are called in the motionEvent. ACTION_MOVE of RecyclerView’s onTouchEvent

caseMotionEvent. ACTION_MOVE: {...if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
        dx -= mScrollConsumed[0];
        dy -= mScrollConsumed[1];
        vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        // Updated the nested offsets
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1]; }...if (mScrollState == SCROLL_STATE_DRAGGING) {
        mLastTouchX = x - mScrollOffset[0];
        mLastTouchY = y - mScrollOffset[1];

        if (scrollByInternal(
                canScrollHorizontally ? dx : 0,
                canScrollVertically ? dy : 0,
                vtev)) {
            getParent().requestDisallowInterceptTouchEvent(true); }...}}break;
Copy the code

It calls onNestedPreScroll of the parent View and passes in dy and consumed. It is used for consumption counting.

OnNestedPreScroll events have different implementations in different parent View, specific can see VerticalNestedScrollLayout in the implementation of the method

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        final ViewParent parent = getNestedScrollingParentForType(type);
        if (parent == null) {
            return false;
        }

        if(dx ! =0|| dy ! =0) {... Consumed [0] = 0;
            consumed[1] = 0; ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type); ...return consumed[0] != 0 || consumed[1] != 0;
        } else if(offsetInWindow ! =null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0; }}return false;
}
Copy the code

ScrollByInternal makes RecyclerView scroll by itself and then dispatchNestedScroll is called

boolean scrollByInternal(int x, int y, MotionEvent ev) {...if(y ! =0) { consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); unconsumedY = y - consumedY; }...if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
            TYPE_TOUCH)) {
        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if(ev ! =null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1]; }...returnconsumedX ! =0|| consumedY ! =0;
}
Copy the code

If we look at the dispatchNestedScroll method, we’re finally calling the parent View’s onNestedScroll method.

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if(dxConsumed ! =0|| dyConsumed ! =0|| dxUnconsumed ! =0|| dyUnconsumed ! =0) {.... ViewParentCompat onNestedScroll (parent, mView, dxConsumed dyConsumed, dxUnconsumed, dyUnconsumed, type);return true;
            } else if(offsetInWindow ! =null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0; }}return false;
    }
Copy the code

So far we can also see that the parent View’s nested scroll method is invoked by the child View, and the child View’s interface is inside the TouchEvent event. Nested scrolling is executed in the following order:

(Child)startNestedScroll → (parent)onStartNestedScroll → (parent)onNestedScrollAccepted→ (child)dispatchNestedPreScroll → (parent)onStartNestedScroll → (parent)onNestedScrollAccepted→ (child)dispatchNestedPreScroll → Parent onNestedPreScroll→ dispatchNestedScroll→ parent onNestedScroll

In the following motionEvent.action_UP:

The nested scroll related fling event resetTouch() is executed by calling the Fling method. The stopNestedScroll event is executed

The process is similar and will not be described. Nested scrolling is executed in the following order:

(Child)startNestedScroll → (parent)onStartNestedScroll → (parent)onNestedScrollAccepted→ (child)dispatchNestedPreScroll → (parent)onStartNestedScroll → (parent)onNestedScrollAccepted→ (child)dispatchNestedPreScroll → (parent) onNestedPreScroll – (sub) dispatchNestedScroll – (parent) onNestedScroll – (sub) dispatchNestedPreFling – onNestedPreFling – > (parent) Dispatchnestedscroll → stopNestedScroll

Auxiliary class NestedScrollingChildHelper and NestedScrollingParentHelper

Since LOLLIPOP(SDK21), nested sliding logic has been written directly into View and ViewGroup classes as a common method. The NestedScrollingChild and NestedScrollingParent interfaces are available in the android.support.v4 compatible package. There are two auxiliary class NestedScrollingChildHelper and NestedScrollingParentHelper to help implement nested sliding control.

Principle of compatibility

The two interfaces, NestedScrollingChild and NestedScrollingParent, define the common methods added to the View and ViewParent mentioned above, respectively

Nested sliding requires that the control either inherits a View or ViewGroup from SDK21 or implements both interfaces, which is a prerequisite for the control to be able to perform nested sliding.

So how do you know if the method called is a control’s own method or an interface method? This is done in code through the ViewCompat and ViewParentCompat classes.

ViewCompat and ViewParentCompat determine the current VERSION by the current build.version. SDK_INT and then select a different implementation class so that you can choose the method to call based on the VERSION.

For example, if the version is pre-SDK21, the control will determine whether the interface is implemented and then call the interface’s methods; if it is post-SDK21, the corresponding method can be called directly.

Reference: https://www.jianshu.com/p/1806ed9737f6

You can also visit our blog to find us, thanks for reading ~