What is nested sliding

Nested sliders are a common UI effect in Android development. When a layout contains multiple views that can be swiped, and these views are nested within each other, nested swiping is required to make the UI interaction smoother, such as the top effect. Common effects are:

As shown above, the outermost parent layout can slide, as can the RecyclerView on the inner layer. When sliding RecyclerView, the outermost parent layout first slide, until the slide to TAB, this time began to slide, the parent layout to stop sliding, and fingers do not need to leave the screen, you can complete the entire operation at once. In this way, the internal and external layout of a coherent sliding effect, and to achieve the TAB top effect.

Second, sliding nesting solution

So how do you do this coherent nested slide?

1. Manual override event distribution and interception

You’re all familiar with Android’s event distribution mechanism, and rewriting event distribution is one of the most primitive ways to achieve nested sliders. Early Android developers did it this way. For example, when the ACTION_MOVE event is distributed, first determine whether the TAB position is at the top. If not, then let the outer parent layout intercept the MOVE event and the parent layout slide. If it has arrived, then do not intercept, the event is passed to the child RecyclerView, the process is as follows:

Intercepting an event, or overriding the onIntercetTouchEvent method, is central to the process.

Disadvantages of manually rewriting event distribution
  • It is only suitable for the simple case of nested sliding

    It makes sense. Because you need to manually write the intercepting logic yourself, once the layout of nested sliders becomes complex, it takes a lot of code and logic to implement nested sliders, increasing maintenance costs. Therefore, it is not suitable for complex nested sliding layout, and in fact difficult to implement complex nested sliding.

  • Difficult to support fling

    Fling is the process by which the view continues to slide with inertia after the hand has been released. In general, nested sliders need to support Fling for user experience. For manual event distribution, not only onInterceptTouchEvent needs to be overwritten, but ACTION_UP event needs to be specifically handled, because Velocity is generated from ACTION_UP event. However, the event distribution mechanism does not provide an exposed interface like onInterceptTouchEvent for developers to handle the ACITON_UP event. You can only do this by copying onTouchEvent and so on, which is very restrictive because you need to call super.onTouchEvent, but you can’t change the code in it.

  • There is no way to achieve a coherent top nesting slide

    Or in the previous example, when TAB suction top, we hope that the fingers do not loosen to continue to slide up can make RecyclerView slide up, but the manual interception event is not to do, must first lift the finger and then slide again. Why is that? Look at the code in dispatchTouchEvent:

    if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {... }else {
      // When MotionEvent is ACTION_MOVE and mFirstTouchTarget == NULL, the event is still intercepted
                    intercepted = true;
                }
    Copy the code

    When a ViewGroup distributes an event, if mFirstTouchTarget == NULL then there are no subviews in the ViewGroup to consume the event. The event is handled by the ViewGroup itself. When the ViewGroup intercepts the event, it simply sets the mFirstTouchTarget blank. Going back to the previous example, when the outer sliding parent layout intercepts the ACTION_MOVE event, it sets the mFirstTouchTarget blank. Then even if the event is not intercepted after the top, because the mFirstTouchTarget has been null, so the event is not passed to the child RecyclerView, but continues to be consumed by the parent layout. This does not achieve a coherent top nesting sliding effect.

CoordinatorLayout + AppBar + Behavior + scrollFlag

CoordinatorLayout is a Set of layouts provided by Google that can be used to achieve complex interactions with appbars, behaviors, and ScrollFlags to decoupage and customize multiple effects. These effects are specified by the Behavior and scrollFlag. And behaviors can be customized.

Implementing nested sliding with CoordinatorLayout is as simple as writing the layout file as follows:


      
<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">// The sliding part<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>
Copy the code

Set scrollFlag to scroll for the part of AppBarLayout that you want to hide. RecyclerView appbar_scrolling_view_behavior appbar_scrolling_view_behavior

It looks like a RecyclerView with a header is sliding, but it’s actually a nested slide.

There are many alternative values for layout_scrollFlags and layout_behavior that can work together to achieve a variety of effects, not just nested slides. Refer to the API documentation for details.

Using CoordinatorLayout for nested sliding is much better than doing it manually, as it allows for consistent top-notch nested sliding with fling support. And is the official provided layout, you can rest assured to use, the probability of a bug is very small, performance will not have a problem. However, because CoordinatorLayout is so well encapsulated officially, it is difficult to implement complex nested slide layouts, such as multi-level nested slides, using CoordinatorLayout.

3. Nested sliders NestedScrollingParent and NestedScrollingChild

NestedScrollingParent and NestedScrollingChild are a set of components that are provided by Google for solving nested sliders. They are two interfaces with the following code:

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);
}

Copy the code

A View that needs to be nested to slide can implement both interfaces, copying the methods in them. The core principle of this set of components to achieve nested sliding is simple, mainly in the following three steps:

  • NestedScrollingChildonTouchEventMethod firstACITON_MOVEThe displacement produced by the event dx and dy passes throughdispatchNestedPreScrollPassed to theNestedScrollingParent
  • NestedScrollingParentonNestedPreScrollIt takes dx and dy and consumes them. And I put in the displacement that I consumedint[] consumed,consumedAn array is an int of length 2,consumed[0]Represents the consumption of the X-axis,consumed[1]This is the consumption on the Y-axis
  • NestedScrollingChildAfter fromint[] consumedArrayNestedScrollingParentYou subtract the displacement that you’ve consumed and you get the rest of the displacement that you can consume by yourself

Slide displacement transfer direction from child -> parent -> child, as shown in the figure below. If the child is Recyclerview, it will first shift to the parent layout consumption, then the parent layout sliding. When the parent layout slides up to the point where it can’t slide, the Recyclerview consumes all the displacement, and then it begins to slide on its own, creating a nested slide, as you saw in the previous example.

DispatchNestedScroll and onNestedScroll work in the same way as preScroll, except that the nested slide order of dispatchNestedScroll is the opposite of preScroll’s. When the child View cannot consume, the parent View consumes again.

The mechanism also supports fling. When the finger leaves the view (ACITON_UP), the Child converts the Velocity to a displacement dx or dy and repeats the process. The value of @nestedscrollType int type is not a TYPE_TOUCH, but a TYPE_NON_TOUCH.

Which Android views use this sliding mechanism?
  • implementationNestedScrollingParentThe View of the interface has:NestedScrollView,CoordinatorLayout,MotionLayout
  • implementationNestedScrollingChildThe View of the interface has:NestedScrollView,RecyclerView
  • NestedScrollViewIs the only View that implements both interfaces at the same time, which means it can be used as a mediation to implement multi-level nested sliding, as we’ll see later.

As you can see above, the CoordinatorLayout implementation is actually implemented through this NestedScrolling interface in essence. But because it’s packaged so well, we can’t do much customization. Using this interface directly, you can customize it according to your own needs.

Most of the scenarios, we do not need to implement the NestedScrollingChild interface, because RecyclerView has done this implementation, and involves a nested sliding scene subview is also basically RecyclerView. RecyclerView RecyclerView

public boolean onTouchEvent(MotionEvent e) {...case MotionEvent.ACTION_MOVE: {
               ...
                // Compute dx, dy
                int dx = mLastTouchX - x;
                intdy = mLastTouchY - y; . mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0; ./ / distribute preScroll
                    if (dispatchNestedPreScroll(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        // Subtracted the displacement consumed by the parent 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;
            // I'm going to consume my scroll first
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            // Calculate the remaining amount
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }

        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
    	// Distribute nestedScroll to parent View in reverse order of preScroll
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1]; . }Copy the code

RecyclerView is how to adjust to the parent View onNestedPreSroll and onNestedScroll? The code for dispatchNestedPreScroll is similar to that for dispatchNestedScroll.

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 instanceofNestedScrollingParent) { ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); }}}Copy the code

As you can see, RecyclerView through a proxy class NestedScrollingChildHelper distribute sliding, finally to ViewCompat handle onNestedPreScroll static method to let the parent View. The main purpose of ViewCompat is to be compatible with different versions of the sliding interface.

Implement the onNestedPreScroll method

From the above code you can clearly see the RecyclerView implementation for NestedScrollingChild, and the timing of triggering nested slides. If we want to implement nested slides and the inner slide child View is RecyclerView, then all we need to do is have the outer parent View implement the NestedScrollingParent method, such as in the onNestedPreScroll method.

 @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
     	// Slide dy distance
       scrollBy(0, dy);
        // Notifies the subview of consumed dy in the consumed array
       consumed[1] = dy;
    }
Copy the code

This makes for the simplest possible nested slide. Of course, in the real world, you have to judge the sliding distance, so you can’t have the parent View consuming the displacement of the child View all the time.

About NestedScrollView

A class like NestedScrollView, because it implements onNestedScroll internally, can slide down in its internal RecyclerView until it reaches the top of the list, and the outer layer continues to slide down without lifting a finger. The onNestedPreScroll method is also implemented, but it continues to pass the slide up in the method without consuming itself, as follows:

// NestedScrollView.java
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            int type) {
    // Only preScroll is distributed and not consumed. It can be distributed because NestedScrollView also implements the NestedScrollingChild interface
        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;
    }
Copy the code

So if directly in the RecyclerView of the outer layer of NestedScrollView is no way to achieve a complete nested sliding, you will find that on the slide, there is no nested sliding effect, and the slide has nested sliding effect.

Problems not considered

In fact, in the previous example, the default is to slide from the child Viw. When the parent View reaches the top, the child View cannot continue to fling, so it stops immediately.

This is because in nested sliders, the displacement consumption can only go from NestedScrollingChild to NestedScrollingParent, not from NestedScrollingParent to NestedScrollingChild, Because only NestedScrollingChild can dispatch, NestedScrollingParent cannot dispatch.

If you want to slide from NestedScrollingParent to NestedScrollingChild, there’s no good way to do it, but to override the parent View’s event distribution and manually distribute the remaining shift from the parent View to its children. (Dig a hole and see if there’s a better way to do this by extending the nested sliding components)

Tips

There are three versions of NestedScrollingParent and NestedScrollingChild.

The first is NestedScrollingParent and NestedScrollingChild. This set of interfaces handles Scroll and Fling separately, creating unnecessary complexity.

Later, NestedScrollingParent2 and NestedScrollingChild2 are inherited from the first generation, but the distance from fling to Scroll is treated in the same way. The above nested sliding components refer to second generation.

And then NestedScrollingParent3 and NestedScrollingChild inherited from the second generation, They add the function of dispatchNestedScroll and onNestedScroll consuming part of the sliding displacement compared to generation 2. That is, after the parent View consumes displacement, the consumed value is consumed in the consumed array to inform the child View. The second generation does not let the child View know the cost of the parent View. Generally speaking, to achieve their own nested sliding, you only need to implement 2 generations and above the interface. Generation ONE is basically no longer used.

Note: One of the important things about using NestedScrollView is that when its subviews are recyclerViews that can be infinitely long, limit the height of those subviews. Don’t use wrAP_content to set the RecyclerView height. As the NestedScrollView gives the subview UNSPECIFIED, that is, UNSPECIFIED, the RecyclerView can be as high as it wishes. Like RecyclerView if the number of internal items is too much, RecyclerView in the case of wrAP_content will show all items, equivalent to no recycling. This is a big memory drain. If you call setVisibility to change the visibility, when it is visible from the invisible, the measurement layout process of all items will be called instantly, resulting in a lag. This is a real problem I have encountered on projects.

Three, multi-level nested sliding

We know that NestedScrollingParent and NestedScrollingChild can be used to customize their own nested sliders. It’s easy to imagine that if a View implements both interfaces, it can accept sliders from the child and distribute sliders to the parent, thus forming a chain. This is where the core principle of multilevel nested sliding comes in, as shown here:

The principle is actually not complicated, as shown in pseudocode:

  • 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; }Copy the code
  • For intermediaries, intermediate views that implement both NestedScrollingParent and NestedScrollingChild

     @Override
        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
            // Distribute first, consume later. It can also be consumed first and then distributed, depending on the business
           dispatchNestedPreScroll(dispatchNestedPreScroll(dx, dy, consumed, null, type);
    	   	 int dx -= consumed[0];
           int dy -= consumed[1];
           scrollByMe(dx, dy);
           consumed[0] = dxConsumed;
           consumed[1] = dyConsumed;
        }
    Copy the code
  • For the innermost NestedScrollingChild, generally use RecyclerView can be.

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.

I don’t want to post the project because it hasn’t been published yet. Here is a picture of jike App’s multi-level nested slide found on the Internet:

You can refer to this article: zhuanlan.zhihu.com/p/56582475

Design patterns used in nested sliding components

To conclude, let’s discuss.

  • The strategy pattern

    NestedScrollingParent and NestedScrollingChild are a pair of interfaces that are implemented by different views to achieve different nested sliding effects. The use of interfaces also ensures scalability.

  • The proxy pattern

    As aforesaid, when a View nested sliding interface to realize the method, the specific transfer slip to the NestedScrollingParentHelper and NestedScrollingChildHelper agent, these two classes is provided by the SDK, The NestedScrollingParent and NestedScrollingChild interfaces are described as follows:

    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.
    Copy the code
  • Adapter mode/appearance mode

    RecyclerView implements the NestedScrollingChild2 interface, but what if its parent view implements the NestedScrollingParent interface? This requires compatibility between different versions of nested sliders. To achieve compatibility, use ViewCompat, 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 instanceofNestedScrollingParent) { ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); }}}Copy the code

    All child sliding distributions are passed to parent via ViewCompat’s static methods, which are compatible with different versions of nested sliding components. At the same time, ViewCompat exposes easy-to-use interfaces and hides compatible processes internally, which can also be seen as a look-and-feel mode.