Problems with nested slides

First nesting slides must involve nesting sliding components. When nesting occurs, sliding events occur and sliding conflicts occur. How do you decide which View slides when two nested views slide?

This problem is usually handled by internal interception and external interception. Either way, however, there is a single object involved in the sliding process. This is a series of sliding events (down->move->… ->move-> UP /cancel), either to the external View or to the internal View, as determined by the traditional event distribution mechanism.

The problem with this, however, is that sliding loses continuity. What we want is for the inner View to scroll to the bottom and then for the outer View to scroll as we continue to slide, a continuity that traditional event distribution cannot provide.

The emergence of nested sliding interfaces

NestedScrollingParent is NSParent and NestedScrollingChild is NSChild.

In order to solve the continuity problem of nested slides, NSParent and NSChild were born. As you can see from the names of the two interfaces, NSParent is the interface that the nested parent layout should implement, and NSChild is the interface that the nested child layout should implement. Through the methods of these two interfaces, the continuity of nested sliding is thus realized.

NSChild and NSParent related methods

There are a number of methods defined in the NSChild and NSParent interfaces that seem very complex and difficult to understand. But if you compare them side by side, it’s easy to understand.

First, open the nesting slide
// NSChild
boolean startNestedScroll(@ScrollAxis int axes);


// NSParent
boolean onStartNestedScroll(
    @NonNullView child, // NSChild's immediate parent layout in NSParent@NonNullView target, // Slide NSChild directly@ScrollAxis int axes);

void onNestedScrollAccepted(
    @NonNull View child,
    @NonNull View target, 
    @ScrollAxis int axes);
Copy the code

Sliding is initiated by NSChild, so when sliding occurs, the NSChild#startNestedScroll method will be called first to enable nested sliding. Similarly, there is a corresponding method in NSParent, that is, onStartNestedScroll. When NSChild starts nesting slide, NSParent can respond to this slide in onStartNestedScroll, that is, whether to participate in the nesting slide. NSParent will receive subsequent method callbacks only after nested slides are established (onStartNestedScroll returns true).

OnNestedScrollAccepted is bound to onStartNestedScroll, and it will be called every time onStartNestedScroll returns true. You can use this method to prepare for nested slides such as parameter initialization.

Two, start nesting slide
1. NSParent preferentially consumes scrolling
// NSChild
boolean dispatchNestedPreScroll(
    int dx, 
    int dy, 
    @Nullable int[] consumed,
    @Nullable int[] offsetInWindow
);


// NSParent
void onNestedPreScroll(
    @NonNull View target, 
    int dx, 
    int dy, 
    @NonNull int[] consumed
);
Copy the code

When the slide starts, the slide event is dispatched via NSChild#dispatchNestedPreScroll, which is consumed first by NSParent#onNestedPreScroll. Then after the NSParent consumes, you need to put the consumed distance into the array consumed. In it, the length of consumed array is 2, which is the consumption of DX and dy respectively.

NSChild#dispatchNestedPreScroll should return true if NSParent consumes the slide event (which can be determined by using consumed array), or false otherwise.

2. Process the slide for NSParent again
// NSChild
boolean dispatchNestedScroll(
    int dxConsumed, 
    int dyConsumed,
    int dxUnconsumed, 
    int dyUnconsumed, 
    @Nullable int[] offsetInWindow
);

// NSParent
void onNestedScroll(
    @NonNull View target, 
    int dxConsumed, 
    int dyConsumed,
    int dxUnconsumed, 
    int dyUnconsumed
);
Copy the code

As mentioned earlier, when a roll occurs, the event is first distributed to NSParent for consumption, and then NSChild for consumption (there is no extra method here, since the whole process should happen in onTouchEvent). After the NSChild is consumed, the remaining events are redistributed to the NSParent. DispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll dispatchNestedScroll

The dispatchNestedScroll method also returns a value, but it does not return whether the event was consumed by NSParent, but whether the event was sent to NSParent this time.

So the nested sliding process is: NSChild produces sliding -> NSParent consumes first -> NSChild consumes again -> all the rest goes to NSParent

Three, start inertial sliding

An inertial slide, or Fling, occurs when the fingers slide quickly and lift. The difference between an inertial slide and a regular slide is that it’s not actually a slide, it’s a speed generated by a finger that slides quickly and lifts up, and then simulates the speed as a slide through the Scroller.

1. Ask the NSParent if it consumes inertial slip
// NSChild
boolean dispatchNestedPreFling(float velocityX, float velocityY);

// NSParent
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
Copy the code

The distribution of inertial sliding is a little different from ordinary sliding, because inertial sliding actually has only one velocity, there is no actual sliding. Thus, when inertia is distributed, only velocity is distributed.

Inertial sliding is also performed by dispatchNestedPreFling. This calls NSParent’s onNestedPreFling method, which NSParent uses to decide whether to participate in the inertial slide or not, and returns true if it does.

2. Tell NSParent if it is sliding inertia
// NSChild
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

// NSParent
boolean onNestedFling(
    @NonNull View target, 
    float velocityX, 
    float velocityY, 
    booleanConsumed // whether NSChild consumes,trueConsumption);
Copy the code

Before the occurrence of inertial sliding, it has been first distributed to NSParent and asked whether it needs to consume inertial speed, and then NSChild decides whether it needs to consume inertial sliding. Finally, the decision to consume speed is sent to NSParent#onNestedFling again via the dispatchNestedFling method (via the consumed parameter).

Why bother? We have already asked whether NSParent needs to consume. Why do we need to tell NSParent his decision after NSChild has made his decision? This is taken into account that when NSParent does not consume inertial slide and NSChild does not consume inertial slide, NSParent can display OverScroll (a blue arc).

End the nesting slide
// NSChild
void stopNestedScroll(a);

// NSParent
void onStopNestedScroll(@NonNull View target);
Copy the code

When the slide is over, you need to call this method to wrap things up. So the process goes like this:

Other methods

NSChild’s other methods

// Set whether the nesting sliding capability is enabled
void setNestedScrollingEnabled(boolean enabled);
// Whether nested sliding is currently supported
boolean isNestedScrollingEnabled(a);
// Whether there is a nested NSParent
boolean hasNestedScrollingParent(a);

Copy the code

Other methods of NSParent

// Get the current scrolling direction
int getNestedScrollAxes(a);
Copy the code
summary

As you can see, if you compare NSChild to NSParent, it makes a lot of sense, because there’s basically a correspondence. And also see in the nested slide, is divided into two parts: one is ordinary slide, one is inertial slide. Ordinary sliding is divided between NSParent and NSChild, but inertial sliding has only one speed, so it is either handled by NSParent or NSChild.

Obviously, ordinary sliding is continuous, but inertial sliding is not continuous. Inertial sliding is still discontinuous because the speed is assigned to only one actor when it occurs.

For example, when NSChild is running out of inertia, when it’s running down to the bottom, what we want is for the external NSParent to continue rolling, but that’s not going to happen by current logic. So when NSChild inertia rolls to the bottom it stops and there is no follow-up.

NSChild2 and NSParent2

Because velocity is unique, it is impossible to break it up and distribute it, so the behavior of inertial sliding can only be handled by one character. Since the inertial sliding cannot be split, another idea is to deal with the inertial sliding by NSChild, and then distribute the sliding generated by the inertial sliding through the nested sliding distribution mechanism.

In this way, inertial sliding can work together in NSChild and NSParent, thus achieving continuity. So NSChild2 and NSParent2 come out.

A change from the first edition

NSChild2 and NSParent2 do not change the original method. Instead, they add an overloaded method to the original method and add an int type to both methods. ViewCompat.TYPE_TOUCH and ViewCompat.TYPE_NON_TOUCH, which stand for normal scroll and inertial scroll.

The reason for adding an overloaded method instead of modifying the original method is compatibility. The methods involved in this change are as follows, which are described above and will not be repeated here:

// NSChild2
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
void stopNestedScroll(@NestedScrollType int type);
boolean hasNestedScrollingParent(@NestedScrollType int type);
boolean dispatchNestedScroll(... @NestedScrollType int type);
boolean dispatchNestedPreScroll(... @NestedScrollType int type);

// NSParent2
boolean onStartNestedScroll(... @NestedScrollType int type);
void onNestedScrollAccepted(... @NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(... @NestedScrollType int type);
void onNestedPreScroll(... @NestedScrollType int type);
Copy the code
summary

You can see that in the second version of nested slides, there is a type parameter for sliding methods, and there is no Fling. Because in the second version of the design, inertial sliding is also as a special ordinary slide, so now it is equivalent to only ordinary slide distribution, it seems that there is no problem.

However, consider a case where NSChild is given a downward inertial slide, and then it will process the inertial slide into a normal slide and distribute it to the NSParent(assuming that the NSParent has now also rolled to the edge). But at this point, press your finger on the NSParent and slide it up. The NSParent won’t move!

This is because when the finger presses on the NSParent, NSChild’s inertial sliding does not cancel, but continuously generates and distributes sliding events. However, the direction of events distributed by NSChild is different from that of the finger sliding NSParent, so there will always be conflicts and cancellations, resulting in immobility.

NSChild3 and NSParent3

When the finger is pressed on NSParent but not in the range of NSChild, the event will not be transmitted to NSChild, that is, NSChild does not know that there is a new event sequence at this time, so it cannot stop the previous inertia sliding event of NSChild when the new event sequence comes.

Therefore, since NSChild cannot actively notify it to stop inertial rolling, it should stop itself. This is the design idea of NSChild3 and NSParent3. It adds an overloaded method to dispatchNestedScroll and onNestedScroll, and an consumed parameter to the second time NSParent consumes it, and returns Void.

In the previous slide event, only the Pre family methods had the CONSUMED array. Because the order of sliding consumption is NSParent first -> NSChild then -> all the rest to NSParent. Therefore, after the first consumption of NSParent, the distance consumed by it needs to be recorded so that NSChild can calculate the distance it can slide. But the last time, I don’t care, because I just end up giving all the remaining distance to NSParent.

In the design of the third edition, the distance consumed by NSParent is also recorded. In this way, when NSChild finds that it is no longer consumed and NSParent is no longer consumed, it means that it can no longer roll. At this time, inertia rolling can be stopped and the scratching problem in the design of the second edition is avoided.

// NSChild3
void dispatchNestedScroll(... @NonNull int[] consumed);

// NSParent3
void onNestedScroll(... @NonNull int[] consumed);
Copy the code

The small sample

NSParent/NSChild are only two interface classes, which define a set of nested sliding standards. We need to implement them ourselves when using them. Of course, so many interface methods are not completely left to us to implement. On Android5.0, these methods are already defined in the View class and the ViewParent interface, so we can use them directly. However, these methods only include the first version methods, not the second and third versions, and are only available after 5.0.

So for the sake of compatibility, and extract the two objects: NestedScrollingParentHelper and NestedScrollingChildHelper. These two Helper classes encapsulate nested sliding related methods and provide implementation. When we customize, we should rewrite all methods of NSParent3 or NSChild3, and use Helper classes to proxy some unnecessary methods.

Define a NestedParent

Define a NestedParent to implement nested sliding with child views. It can have two child views. The first child View is a Header, which will be rolled out first when scrolling.

The source code

Click on the Github link

other

You can also check out this article, the method introduction + small example, I learned Behavior, in fact, many effects are more convenient to use Behavior encapsulated nested sliding.

conclusion

1. Nested sliding is initiated by NSChild.

2. The order of nested sliding is: NSChild initiates ->NSParent processes first ->NSChild processes again -> the rest is sent to NSParent

3. The distribution of inertial sliding speed is cancelled in nested sliding, but is directly converted into ordinary sliding in NSChild for nested distribution.

4. NSChild needs to proactively cancel inertial sliding according to the consumption of NSParent in the last step.