Android nested sliding summary

1. What is nested sliding

Nested sliding is a common UI effect in android development. When a layout contains multiple slidable Views, and these Views are nested with each other, you need to do nested sliding processing to make the UI interaction have a smoother effect, such as a ceiling effect. The common effects are as follows:

As shown above, the outermost parent layout can slide, and the inner RecyclerView can also slide. When sliding up the RecyclerView, the outermost parent layout first slides up until it reaches the tab. At this time, the RecyclerView starts to slide, the parent layout stops sliding, and the finger does not need to leave the screen, and the entire operation can be completed at one time. In this way, the effect of continuous sliding of the internal and external layout is achieved, and the effect of tab ceiling is achieved.

Two, sliding nesting solution

So how can we achieve such a coherent nested sliding?

1. Manually override event distribution and interception

Everyone is familiar with Android's event distribution mechanism. To achieve the effect of nested sliding, rewriting event distribution is the most primitive method. Early android developers did this. Take the effect shown before as an example. When the ACTION_MOVE event is distributed, first determine whether the tab position is to the top. If it is not, let the outer parent layout intercept the MOVE event, and the parent layout slides. If it has already arrived, do not intercept and pass the event to the child RecyclerView. The process is as follows:

Whether intercept events that override the onIntercetTouchEventmethod, is at the heart of the process.

Disadvantages of manually rewriting event distribution
  • Only suitable for relatively simple nested sliding situations

    This is easy to understand. Because you need to manually write the interception logic yourself, once the layout of nested sliding is complicated, a lot of code and logic are needed to implement nested sliding, which increases the cost of maintenance. Therefore, it is not suitable for complex nested sliding layout, and it is actually difficult to realize complex nested sliding.

  • Hard to support fling

    Fling refers to the process in which the view continues to slide by inertia after the sliding is released. Generally speaking, considering the user experience, nested sliding needs to support fling. So for the preparation of a manual event distribution, in addition needs to be rewritten onInterceptTouchEvent, but also the need for ACTION_UPfor specific processing event, because due to fling ACTION_UPgenerated event Velocity. However, the event distribution mechanism does not provide like onInterceptTouchEventthat kind of exposure to external interface allows developers to handle the ACITON_UPevent. Only through replication onTouchEventto deal with other methods, but doing so too restrictive, because you need to call super.onTouchEvent, but you can not modify the code.

  • There is no way to achieve coherent ceiling nesting sliding

    Taking the previous example again, when the tab is on top, we hope that the finger will not release and continue to slide up to make the RecyclerView slide up. However, it is impossible to manually intercept the event. You must lift your finger first. Then slide again. Why is this happening? Take a look at dispatchTouchEventthe code:

    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                    ......
                } else {
      // 当 MotionEvent 为 ACTION_MOVE 且 mFirstTouchTarget == null 时,仍然拦截事件
                    intercepted = true;
                }
    

    When the distribution ViewGroup event, if mFirstTouchTarget == nullit indicates there are no sub ViewGroup View to the consumer event, which is handled by ViewGroup own. And when ViewGroup intercept events, will just mFirstTouchTargetbe empty. Back to the previous example, when the father of the outer slide layout intercepted ACTION_MOVEafter the event, it will be mFirstTouchTargetblank. Then do not intercept events even after the ceiling, as mFirstTouchTargetalready null, so the event will not be passed to the child RecyclerView, but continue to consume by the parent layout. In this way, the continuous sliding effect of the ceiling nesting is not achieved.

2、CoordinatorLayout + AppBar + Behavior + scrollFlag

CoordinatorLayoutIs a complex interaction effects can be achieved provided by google layout, and AppBar, Behavior, scrollFlagwith the use of decoupling can customize a variety of effects that made Behaviorand scrollFlagspecified. And Behaviorcan be customized.

Use CoordinatorLayoutimplement nested slide is very simple, as long as the preparation of the layout file as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_height="300dp"
        android:layout_width="match_parent">

      	// 可滑动部分
        <View
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            app:layout_scrollFlags="scroll"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="64dp"
            android:layout_gravity="bottom"
            android:text="Top"
            android:textSize="32sp"
            android:textColor="@color/white"
            android:gravity="center"
            android:textStyle="bold"/>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

The AppBarLayoutneed to hide the slide portion scrollFlagis designated as scrollspecified in the RecyclerView behavioris appbar_scrolling_view_behavioreasiest ceiling nested slide can be achieved, as follows:

It looks like the RecyclerView with header is sliding, but it is actually a nested sliding.

layout_scrollFlagsAnd layout_behaviorthere are many possible values, together with the various effects can be achieved, is not limited to the slide nest. For details, please refer to the API documentation.

Use CoordinatorLayoutthe nesting achieve much better than the manual sliding, sliding nested ceiling can be achieved only consistent, but also support fling. And it is the official layout, you can use it with confidence, the chance of bugs is very small, and the performance will not be problematic. But it is precisely because the official encapsulates very well, use CoordinatorLayoutis difficult to achieve more complex nested slide layouts, such as multi-level nested slide.

3. Nested sliding components NestedScrollingParent and NestedScrollingChild

NestedScrollingParentAnd NestedScrollingChildis the official google to solve a set designed to slidably nested components. They are two interfaces, the code is as follows:

public interface NestedScrollingParent2 extends NestedScrollingParent {

    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            @NestedScrollType int type);

}

public interface NestedScrollingChild2 extends NestedScrollingChild {

    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    void stopNestedScroll(@NestedScrollType int type);

    boolean hasNestedScrollingParent(@NestedScrollType int type);

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);
}

Views that require nested sliding can implement these two interfaces and rewrite the methods in them. The core principle of this set of components to achieve nested sliding is very simple, mainly the following three steps:

  • NestedScrollingChildIn the onTouchEventmethod of the first ACITON_MOVEdisplacement dx and dy events produced by dispatchNestedPreScrollpassing to theNestedScrollingParent
  • NestedScrollingParentIn onNestedPreScrollthe received dx and dy and consumption. Displacement into and consumed int[] consumed, the consumedarray is an array of type int length of 2, consumed[0]representative of the x-axis consumption, consumed[1]representative of the y-axis consumption
  • NestedScrollingChildAfter the int[] consumedget the array NestedScrollingParenthas been consumed displacement, obtained after subtracting the remaining displacement, and then by their own consumption

The transfer direction of the sliding displacement is from child -> parent -> child, as shown in the figure below. If the child is a Recyclerview, it will first transfer the displacement to the parent layout for consumption, and then the parent layout slides. When the parent layout slides to the point where it cannot slide, the Recyclerview consumes all the displacement at this time. At this time, it starts to slide by itself, thus forming a nested slide. The effect is as seen in the previous example.

Displacement transmission process

dispatchNestedScrollAnd onNestedScrollthe like of the action principle preScroll above, but this method of constructing two nested slide and preScroll reverse order, the consumer is the first child View, View child can not consume time, and then again by the parent View consumption.

This mechanism also supports fling, when the finger leaves the view, that is generated ACITON_UPwhen an event, child at this time Velocityinto the displacement dxor dyflow before and repeat. By @NestedScrollType int typejudged value TYPE_TOUCHor TYPE_NON_TOUCH, TYPE_TOUCHis slidable, TYPE_NON_TOUCHis fling.

Which Views in Android use this sliding mechanism?
  • Implement NestedScrollingParentthe interface View NestedScrollVieware: CoordinatorLayout, , MotionLayoutetc.
  • Implement NestedScrollingChildthe interface View NestedScrollVieware: , RecyclerViewetc.
  • NestedScrollView It is the only View that implements two interfaces at the same time, which means that it can be used as an intermediary to implement multi-level nested sliding, which will be discussed later.

Can be seen from the above, in fact, mentioned before CoordinatorLayoutnested slide implemented, it is essentially achieved by this NestedScrolling interface. But because it is packaged so well, we cannot do too much customization. And directly using this set of interfaces, you can customize it according to your own needs.

Most of the scenes, we do not need to implement NestedScrollingChildthe interface, because RecyclerView has been done to achieve this, and it comes to the nested sub-slide scene View are also essential RecyclerView. Let's take a look at the relevant source code of RecyclerView:

public boolean onTouchEvent(MotionEvent e) {
    ...
    case MotionEvent.ACTION_MOVE: {
               ...
                // 计算 dx,dy
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
				...
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
       				...
                    // 分发 preScroll
                    if (dispatchNestedPreScroll(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        // 减去父 view 消费掉的位移
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];

                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];

                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
        ...
            } break;
    ...
}

boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            // 先消耗掉自己的 scroll
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            // 计算剩余的量
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }

        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
    	// 分发 nestedScroll 给父 View,顺序和 preScroll 刚好相反
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
		...
    }

RecyclerView how is transferred to the parent View, onNestedPreSrolland onNestedScrollit? Analyze dispatchNestedPreScrollthe code below, dispatchNestedScrollthe principles and the code is similar, no longer posted:

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

// NestedScrollingChildHelper.java
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 false;
    }

// ViewCompat.java
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    parent.onNestedPreScroll(target, dx, dy, consumed);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onNestedPreScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }
    }

You can see, RecyclerView through a proxy class NestedScrollingChildHelpersliding distribution is completed, the last to the ViewCompatstatic method of treatment to make the parent View onNestedPreScroll. ViewCompatThe main function is to be compatible with different versions of the sliding interface.

Implement the onNestedPreScroll method

From the above code can be clearly seen RecyclerView for NestedScrollingChildimplementation nested slide and the trigger timing. If we are to achieve nested slide and slide inside child View is RecyclerView, then just let the outer layer of the parent View implementations NestedScrollingParentmethod on the line, such as in the onNestedPreScrollmethod,

 @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
     	// 滑动 dy 距离
       scrollBy(0, dy);
        // 将消耗掉的 dy 放入 consumed 数组通知子 view
       consumed[1] = dy;
    }

This achieves the simplest nested sliding. Of course, in the actual situation, the sliding distance must be judged, and the parent view cannot always consume the displacement of the child view.

About NestedScrollView

Like NestedScrollViewthis class because it implements the inside onNestedScroll, so in the fall, it fell in RecyclerView inside the top of the list until the outer continue to decline without lifting your finger. There is also a realization onNestedPreScrollmethod, but it continues to slide upward in the transfer process, not their consumption, the following code:

// NestedScrollView.java
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            int type) {
    // 只分发了 preScroll 自己并没有消费。之所以能分发是因为 NestedScrollView 同时实现了 NestedScrollingChild 接口
        dispatchNestedPreScroll(dx, dy, consumed, null, type);
    }

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

// NestedScrollingChildHelper.java
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 false;
    }

So if directly RecyclerView outer jacket NestedScrollViewis no way to achieve complete nested slides, you will find when on slippery, no nested slide effect, but fell when nested slide effect.

Unconsidered issues

In fact, in the content mentioned before, it is assumed that the finger starts to slide from the sub Viw. If the finger starts to slide from the outer parent View, when the parent View flings to the top, the child View cannot continue to fling, and will stop immediately, failing to achieve continuous nested sliding.

This is because the nested slide assembly, the shift from consumption only NestedScrollingChildto NestedScrollingParent, not from NestedScrollingParentto NestedScrollingChild, because only NestedScrollingChildin order to dispatch, NestedScrollingParentcan not dispatch.

If you want to achieve from NestedScrollingParentthe NestedScrollingChildslide coherent, there is no particularly good way, can only override the parent View event distribution, after the remainder of the top parent View slide displacement manually distributed to its sub-View. (First dig a hole to see if there is a better way, you can achieve the goal by expanding the nested sliding component)

Tips

NestedScrollingParentAnd NestedScrollingChilda total of three versions.

The earliest is NestedScrollingParentand NestedScrollingChildwhich set of interfaces to scroll and fling processed separately, resulting in unnecessary complexity.

After that I came NestedScrollingParent2and NestedScrollingChild2inherited from the previous generation, but it will fling into the distance scroll the reunification process. The above-mentioned nested sliding components all refer to the second generation.

Subsequently, a further NestedScrollingParent3and NestedScrollingChildinherited from the second generation, which increases compared to the second generation dispatchNestedScroll, and onNestedScrollcan consume part sliding displacement, i.e. displacement after the parent View consumption, the consumption value into the consumedarray of notification sub-View. The second generation will not let the child View know the consumption value of the parent View. Generally speaking, if you want to implement nested sliding yourself, you only need to implement the 2nd generation and above interface. The first generation is basically no longer used.

Note : Use NestedScrollViewa need to pay attention, when it is RecyclerView child View this when content can be infinitely long layout, should be taken to limit the height of the sub-View, do not use the wrap_contentset RecyclerView height. Because NestedScrollViewwhen measuring a child View restriction is UNSPECIFIEDthat no limit, RecyclerView how high you want to have tall. Like RecyclerView if too many internal item, RecyclerView in wrap_contentthe case of all item will show up, equivalent to no recovery. This will cause a lot of memory consumption. If you call setVisibilityto change visibility, then, when from invisible to visible, but will instantly call to all item test layout process, resulting in Caton. This is the problem I actually encountered in the project.

Three, multi-level nested sliding

We know that NestedScrollingParentand NestedScrollingChildcan be used to customize realize their nest slide. It is easy to think that if a View implements two interfaces at the same time, it can not only accept the sliding of the child, but also distribute the sliding to the parent, thus forming a chain. The core principle of multi-level nested sliding comes from this, as shown in the figure:

image-20210304170155409.png

The principle is actually not complicated. Let's show it in pseudo code below:

  • for NestedScrollingParent

     @Override
        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
           scrollByMe(dx, dy);
           ...
           consumed[0] = dxConsumed;
           consumed[1] = dyConsumed;
        }
    
    
  • For intermediaries, that is, while achieving NestedScrollingParentand NestedScrollingChildmiddle View is

     @Override
        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
            // 先分发,再消费。当然也可以先以先消费,再分发,这取决于业务
           dispatchNestedPreScroll(dispatchNestedPreScroll(dx, dy, consumed, null, type);
    	   	 int dx -= consumed[0];
           int dy -= consumed[1];
           scrollByMe(dx, dy);
           consumed[0] = dxConsumed;
           consumed[1] = dyConsumed;
        }
    
    
  • For the innermost layer NestedScrollingChild, RecyclerView can generally be used.

In the multi-level nested sliding, you can set the priority of each layer in the process of sliding up and down according to the business.

The working project will not be put up because it has not been released. Here is the image of the multi-level nested sliding of the instant app found on the Internet:

Fourth, the design pattern used in nested sliding components

As a summary, discuss it.

  • Strategy mode

    NestedScrollingParentAnd NestedScrollingChilda pair of interfaces, the different interfaces of View to achieve different pair of nested achieved by sliding effect. At the same time, the use of interfaces also ensures scalability.

  • Agency model

    As described above, when the method of nesting a sliding interface View implementations, the transfer carriage are specific to the agent NestedScrollingParentHelperand NestedScrollingChildHelperto achieve, which is provided by two classes sdk in NestedScrollingParentand NestedScrollingChildfound the following interfaces:

    This interface should be implemented by ViewGroup subclasses
    that wish to support scrolling operations delegated by a nested child view.
    Classes implementing this interface should create a final instance of a
    NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods
    to the NestedScrollingParentHelper methods of the same signature.
    
    
  • Adapter Mode / Appearance Mode

    RecyclerView implements NestedScrollingChild2the interface, but if its parent view to achieve a generation of NestedScrollingParentinterfaces how to do? This means that different versions of nested sliding components need to be compatible. How to achieve compatibility, use ViewCompatit as follows:

    // ViewCompat.java
    public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
                int[] consumed, int type) {
            if (parent instanceof NestedScrollingParent2) {
                // First try the NestedScrollingParent2 API
                ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
            } else if (type == ViewCompat.TYPE_TOUCH) {
                // Else if the type is the default (touch), try the NestedScrollingParent API
                if (Build.VERSION.SDK_INT >= 21) {
                    try {
                        parent.onNestedPreScroll(target, dx, dy, consumed);
                    } catch (AbstractMethodError e) {
                        Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                                + "method onNestedPreScroll", e);
                    }
                } else if (parent instanceof NestedScrollingParent) {
                    ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
                }
            }
        }
    
    

    Sliding distribute all of the child, will by ViewCompatstatic method final before being passed to parent, through this class and static methods to achieve the compatibility with different versions of nested slide assembly. At the same time, ViewCompatthe easy-to-use interface is exposed to the outside, and the compatible process is hidden inside itself, which can also be seen as a kind of appearance mode.

Guess you like

Origin blog.csdn.net/zhireshini233/article/details/115034973