(Please indicate the author: RubiTree, address:blog.rubitree.com )

NestedScrolling (mixed in this article) provides an elegant way to handle NestedScrolling. Let’s start with nested cosliding.

1. Nested cosliding

1.1. The problem of nested cosliding

Nested cosliding is a situation where two sliding views are nested inside and out, and they slide in the same direction.

In this case, if the general processing method is used, interaction problems will occur. For example, when two ScrollViews are used for layout, you will find that when you touch the internal ScrollView and slide it, it will not slide (not considering the NestedScroll switch added by Google later) :

1.2. Analyze the cause of the problem

(Warm reminder: This article involves a lot of content about event distribution. It is recommended that students who are not familiar with event distribution read another article “Through Lens > Touch Event Distribution” first.)

If you’re familiar with Android’s touch event distribution mechanism, it’s easy to understand why: When two ScrollViews are nested, a MOVE event that reaches the sliding threshold (mTouchSlop) will pass through the parent View’s onInterceptTouchEvent() method. The parent View then intercepts the event directly. Child View onTouchEvent () method in although will call after judged sliding distance enough requestDisallowInterceptTouchEvent (true), but always late.

And this is obviously not intuitive what do users want to see?

  1. Most of the time, the user wants to see: when the finger touches the insideScrollViewWhen you slide, you can slide inside firstScrollView, only when insideScrollViewWhen you get to the end, slide the outsideScrollView

This seems very natural and is consistent with the way touch events are handled, but it is much harder to achieve the same effect when sliding than when touch events are handled

  1. For sliding movement could not immediately identified, its processing itself need through the event interceptor mechanism, and event interceptor mechanism in essence with the distributed through > touch events, for the first time made the wheels of the same, is only one way, and the direction from outside to inside, so I can’t do it: let the internal block sliding, internal not intercept sliding, again in let external intercept sliding

How about making event blocking bidirectional? Not impossible, but this clearly defeats the purpose of interception, and it will soon become infinitely recursive: does two-way event interception itself need an interception mechanism? So there were intercepts and then intercepts and intercepts…

1.3. Try to solve the problem

For a more direct way of thinking, if our demand is always internal sliding first, can we make the external View “intercept sliding judgment conditions” more strict than the internal View “apply external non-intercept judgment conditions”, so that the sliding distance each time first reach “apply external non-intercept judgment conditions”, The child View will be able to request external non-blocking before the parent View intercepts the event. Math.abs(deltaY) > mTouchSlop, we just need to increase the mTouchSlop in the case of “intercept slide criteria”.

This is not a good idea, however, because it is uncertain how much mTouchSlop should be increased, and the speed of finger swiping and screen resolution may affect it. So it can be implemented in another way, that is, when the first “judgment condition of interception slide” is established, the first interception is not carried out. If the internal application is not applied for, the external interception is not carried out, when the second condition is established, the interception is carried out, which also realizes the idea at the beginning. Inheriting ScrollView and overwriting its onInterceptTouchEvent() :

class SimpleNestedScrollView(context: Context, attrs: AttributeSet) : ScrollView(context, attrs) {
    private var isFirstIntercept = true
    
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            isFirstIntercept = true
        }

        val result = super.onInterceptTouchEvent(ev)

        if (result && isFirstIntercept) {
            isFirstIntercept = false
            return false
        }

        return result
    }
}    
Copy the code

It works like this, and you can see that it really does get the event internally first:

1.4. First optimization

But we want the experience to be a little bit better. As you can see from the image above, the inside intercepts events even when it can’t slide itself, and there’s no way to slide the inside to make the outside slide. In fact, the internal should return false in onTouchEvent() when it cannot slide, so that both internal and external can slide without triggering the “request for external not blocking criteria”. This requirement is very generic and reasonable, with a simple modification based on SimpleNestedScrollView and the following code:

private var isNeedRequestDisallowIntercept: Boolean? = null

override fun onTouchEvent(ev: MotionEvent): Boolean {
    if (ev.actionMasked == MotionEvent.ACTION_DOWN) isNeedRequestDisallowIntercept = null
    if (ev.actionMasked == MotionEvent.ACTION_MOVE) {
        if (isNeedRequestDisallowIntercept == false) return false

        if (isNeedRequestDisallowIntercept == null) {
            val offsetY = ev.y.toInt() - getInt("mLastMotionY")
            if (Math.abs(offsetY) > getInt("mTouchSlop")) { // The sliding distance is enough to determine whether the sliding direction is up or down
                // Determine if you can slide in the corresponding direction (if not, return false)
                if ((offsetY > 0 && isScrollToTop()) || (offsetY < 0 && isScrollToBottom())) {
                    isNeedRequestDisallowIntercept = false
                    return false}}}}return super.onTouchEvent(ev)
}

private fun isScrollToTop(a) = scrollY == 0

private fun isScrollToBottom(a): Boolean {
    return scrollY + height - paddingTop - paddingBottom == getChildAt(0).height
}
Copy the code
  1. Among themgetInt("mLastMotionY")andgetInt("mTouchSlop")Gets private for reflection codemLastMotionYandmTouchSlopattribute
  2. This code omits the multi-touch judgment

The running effect is as follows:

This completes the most basic requirement for nested sliding views: everyone can slide.

Later, I found a more wild way, no need to carefully make the change as small as possible, since internal first, you can make the internal ScrollView apply for external blocking when the DOWN event, and then cancel the external blocking restriction if it determines that it cannot slide in the sliding direction after a certain distance. The idea is similar but the code is simpler.

class SimpleNestedScrollView(context: Context, attrs: AttributeSet) : ScrollView(context, attrs) {
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) parent.requestDisallowInterceptTouchEvent(true)
        
        if (ev.actionMasked == MotionEvent.ACTION_MOVE) {
            val offsetY = ev.y.toInt() - getInt("mLastMotionY")

            if (Math.abs(offsetY) > getInt("mTouchSlop")) {
                if ((offsetY > 0 && isScrollToTop()) || (offsetY < 0 && isScrollToBottom())) {
                    parent.requestDisallowInterceptTouchEvent(false)}}}return super.dispatchTouchEvent(ev)
    }
}
Copy the code

Run the effect is the same as above, do not repeat the map.

1.5. Second optimization

But neither of these approaches so far provides the best interactive experience. The best interactive experience is one where the inside can’t slide and then the outside can slide, and even when you lift quickly as you slide, the following inertial slide can be passed between the two sliding views.

Because of the unique nature of the sliding interaction, we can operate on it externally, so the implementation of continuous sliding is very simple, just rewrite scrollBy, so add the following code to the existing code (both of the above are the same code) :

override fun scrollBy(x: Int, y: Int) {
    if ((y > 0 && isScrollToTop()) || (y < 0 && isScrollToBottom())) {
        (parent as View).scrollBy(x, y)
    } else {
        super.scrollBy(x, y)
    }
}
Copy the code

The effect is as follows:

The implementation of inertial sliding is a bit more complicated and requires computeScroll(), which requires more modifications. I won’t implement it for now, but I’m sure I can do it.

1.6 summary

Up to now, our understanding of nested sliding interaction has been very transparent. Now that we know that it is only right to implement it ourselves, we mainly need to solve the following problems:

  1. When the inner View can slide, prevent the outer View from intercepting the sliding event, and slide the inner View first
  2. In a user slide operation, when the internal View slides to the end point, the sliding object is switched to the external View, so that the user can slide continuously
  3. In the inertia sliding triggered by the user’s quick lifting, when the internal View slides to the end point, the sliding object is switched to the external View, so that the inertia can be continuous

How does the system provide NestedScrolling for NestedScrolling? Is it better or not?

(Please indicate the author: RubiTree, address:blog.rubitree.com )

2. NestedScrolling mechanism

Principle of 2.1.

Unlike us, we have only considered adding nested slides to ScrollViews, but system developers need to consider adding this feature to all views that interact with slides, so a direct thought would be to add this mechanism to views.

So how do you add, what do you add?

  1. In nested sliding, two classes of objects can be clearly distinguished: one is an internal View, and one is an external View. And the relationship between them is very clear: since the inner View is closer to the finger, we definitely want it to consume events first, but we also want events to consume events externally when they are not consumed internally, which of course is also controlled internally, soInternally active, externally passive(Back to air motor)
  2. Thus, the whole process of nested sliding can be considered as follows: touch events are handed to the internal View for consumption, and the internal View performs relevant logic, and controls the external View to some extent when appropriate, and the two cooperate to realize nested sliding
  3. There are two parts to this logic:
    1. Active logic in internal View: You need to actively prevent external View from intercepting events, you need to do your own sliding, and when appropriate, let the external View cooperate to do the rest of the sliding
      1. This is the core, and this is what we implemented ourselves earlier
    2. Passive logic in external views
      1. It’s basically a coordinated action. There’s not much logic to that part
  4. Due to theViewYou can’t put anything else in thereViewIt can only be an internal, active role, andViewGroupYou can put it on the other sideViewGroupIt can also put other things in itViewSo it can be an internal or an external role
  5. That fits perfectlyViewandViewGroupSo a very natural design is: inViewTo add active logic inViewGroupAdd passive logic to

Because not every View and ViewGroup can swipe, swipe is just one of many interactions, and a View and ViewGroup can’t just do everything and tell you: Android supports nested sliding, so Google added these logic is actually a help method, the relevant View needs to be selected at the appropriate time to achieve the effect of nested sliding.

Not to mention what methods have been added, but what kind of nested sliding effect Google wants to help you achieve:

  1. Logically distinguish between two roles in a nested slide:ns childandns parent, corresponding to the inner View and outer View above
    1. Note: 1) I use “ns” herenested scrollThe abbreviation of; 2) Why is it called logically? Because it actually allows you to have a View play two roles at once
  2. ns childWill be receivedDOWNEvent, find their ancestors in the most recent can match their ownns parentBind to it and turn off its event blocking mechanism
  3. thenns childIt will be in the nextMOVEEvent determines that the user triggered the swipe gesture, and intercepts the event stream for their own consumption
  4. Consume event streams for each timeMOVEEvent increases slip distance:
    1. ns childYou don’t just spend it yourself, you give it to them firstns parentAnd letns parentCan be found inns childPrevious consumption slide
    2. ifns parentNot consuming or not consuming,ns childThen consume the rest of the swipe
    3. ifns childIf you still haven’t consumed this slide, you will hand over the rest of the slidens parentconsumption
    4. At the end of the slide, if I have anything left,ns childYou can do the final processing
  5. At the same timens childthecomputeScroll()Method,ns childWill also put themselves because of the userflingThe swipe triggered by the action is the same as the swipe triggered by the user swiping the screen in the previous slide, using the order “parent -> child -> parent -> Child” for consumption

Note:

  1. The above procedures refer to the current latestAndroidx. Core 1.1.0 - alpha01In theNestedScrollViewandAndroidx. RecyclerView 1.1.0 - alpha01In theRecyclerViewThe implementation is slightly different from the previous version in detail, which will be discussed later
  2. For ease of understanding, a few details have been simplified: in fact, inNestedScrollView,RecyclerViewIn this class of classical implementations: 1ns childScrolling, as long as the user’s finger presses,ns childThe stream of events will be intercepted without having to judge the sliding gesture (see source code for details)mIsBeingDraggedField) 1. This detail is reasonable and will make the user experience better 2. (This detail will not be explained later, but will be directly used in a simplified description. According to Android’s touch event distribution rules, ifns childThere are no internal views to consume events, and events will be handed in directlyns childonTouchEvent()Consumption. At this moment inNestedScrollViewns childIn the implementation ofinonTouchEvent()Before determining whether the user is going to slide itself, it will hand over the user’s slidens parentTo spend(Back to 4.4)1. I personally don’t think the design is reasonable, since it is sliding it should pass in the judge the user really after sliding to, rather than directly, but also the schematics of the practice part, you can actually see the problem with this design 1. (later described if there are no special instructions, is also the default to ignore this detail)
  3. The part about passing the fling directly is omitted from the description because of the design issues and the use of this mechanism is very small in the latest version, more on this later

You’ll see that this is very similar to the way we implement nested sliding ourselves, but it does a lot better with these things (see more on how to do this later).

  1. ns childUse a more flexible way to find and bind your ownns parentInstead of looking for your last node
  2. ns childinDOWNEvent closens parentThe event interception mechanism of the Flag is turned off with a single Flag, which will not be turned offns parentInterception of other gestures does not recursively turn off the ancestors’ event interception mechanism.ns childUntil theMOVEEvent to determine that you want to start slidingrequestDisallowInterceptTouchEvent(true)Recursively turns off all ancestor event interception
  3. For each timeMOVEEvent passes to the slider, using the “parent -> child -> parent -> child” mechanism for consumption, letns childWhen consumption slidesns parentCoordination is more detailed, close and flexible
  4. For the userflingThe swiping triggered by the operation uses the same mechanism as the swiping triggered by the user sliding the screen for consumption, achieving a perfect inertial continuous effect

2.2. Use

At this point, let’s take a look at what methods Google has added to views and Viewgroups. And how and when do you want us to call them?

There are some methods you need to care about (only the key return values and parameters are noted, refer to the current latest version of Androidx.core 1.1.0-alpha01) :

/ / "View"
setNestedScrollingEnabled(true)                       / / call
startNestedScroll()                                   / / call
dispatchNestedPreScroll(int delta, int[] consumed)    / / call
dispatchNestedScroll(int unconsumed, int[] consumed)  / / call
stopNestedScroll()                                    / / call

/ / "ViewGroup"
boolean onStartNestedScroll()                       / / overwrite
int getNestedScrollAxes()                           / / call
onNestedPreScroll(int delta, int[] consumed)        / / overwrite
onNestedScroll(int unconsumed, int[] consumed)      / / overwrite
Copy the code

How you call these methods depends on what role you’re implementing, right

  1. Before you achieve onens childTo play the role, you need:
    1. Called at instantiation timesetNestedScrollingEnabled(true)To enable the nesting sliding mechanism
    2. inDOWNEvent time callstartNestedScroll()Method, it will “find the closest match in its ancestryns parentTo bind and closens parentEvent interception mechanism”
    3. After determining that the user is swiping
      1. First general operation: close all ancestor event interception, while blocking their own child View events
      2. And then calldispatchNestedPreScroll()Method, passed in to the user’s slide distance, this method will “firens parentSwipe consumption, and return consumption results.”
      3. thenns childYou can start your own consumption by swiping
      4. ns childCall after their consumptiondispatchNestedScroll()Method, passing in the last unconsumed slide distance, the method will continue to “fire.ns parentSwipe the remaining consumption and return the consumption result.”
      5. ns childTake the last scroll that has not been consumed and perform a final action, such as displaying the overscroll or stopping the scrollscroller
    4. If you want inertial sliding to be transmitted tons parentAnd so onViewthecomputeScroll()Method, for eachscrollerCalculated to slip distance, andMOVEEvents are processed in the same order as slides, consuming in this order:”dispatchNestedPreScroll()– > – >dispatchNestedScroll()– > yourself”
    5. inUP,CANCELIn the event andcomputeScroll()Method in the inertial slide ends when calledstopNestedScroll()Method, which will “openns parentAnd unbind it.”
  2. Before you achieve onens parentTo play the role, you need:
    1. Overriding methodsboolean onStartNestedScroll(View child, View target, int nestedScrollAxes), by passing in the parameters, determine your interest in this kind of nested slide, and return in the case of interesttrue.ns childBy going through all of themns parentThis method to find their matchns parent
    2. Call before intercepting the slide event if support for nested sliding is selectedgetNestedScrollAxes()It will return to you if the intercepting mechanism in a certain direction has beenns childIt’s off. If it’s off, you shouldn’t intercept the event
    3. After enabling the nesting slide, you can use theonNestedPreScrollandonNestedScrollMethod to wait patientlyns childThat’s right, it corresponds to yourns childIn the calldispatchNestedPreScrollanddispatchNestedScrollMethod, you can do your own sliding as necessary and return the lost sliding distance through the array in the parameters

So implementation examples we can see ScrollView, as long as the open it setNestedScrollingEnabled (true) switch, you can see the nested sliding effects: (ScrollView actually not perfect nested sliding, because see the next section)

Ns parent is fine, but there are a lot of details about the implementation of NS Child (including “event correction caused by ns parent offset” and so on). It may not be straightforward enough to describe, so I have prepared a reference template for ns Child: NestedScrollChildSample

Pay attention to

  1. Although templates do not report errors in the IDE, this is not runnable code, this is cullingNestedScrollViewIn aboutns parentCan be regarded as the official recommendationns childimplementation
  2. At the same time, in order to make the main line logic clearer, the multi-touch related logic is deleted, which can be directly referred to in the actual development if necessaryNestedScrollView* (I will write multi-touch lens series XD) *
  3. The key part of this is how do you call it when you touch and when you scrollNestedScrollingChildThe method of the interface, which isonInterceptTouchEvent()onTouchEvent()computeScroll()About 200 lines of code in

In addition, the above is the use of a single role, sometimes you need a View to play two roles, you need to do more things, for example, for ns parent, you should always pay attention to you are also NS child, and take care of your own NS parent when coming to business. You can look at the implementation of NestedScrollView, I’m not going to expand it here.

(Please indicate the author: RubiTree, address:blog.rubitree.com )

3. Age: 43

But then someone asked :(back to answer)

  1. How do I see people saying you have to achieveNestedScrollingParentandNestedScrollingChildThese two interfaces, and then we useNestedScrollingParentHelperandNestedScrollingChildHelperThese two help classes can implement a custom View that supports nested sliding, and we all appreciate this is a great design, how to you this is directly added to the View and ViewGroup method, such a common DISCO? And the picture also saw that there are several interfaces ah, you are the title party?(Nice to remember the picture)
  2. Why do you implement nested sliding without implementing interfaces, and why do almost all views that implement nested sliding implement both interfaces?
  3. Why is it clear that the nested sliding mechanism is inNestedScrollingParentandNestedScrollingChildThere are so many methods in these two interfaces, but you only talk about nine?
  4. Why don’t you explain the fling method in the interface?
  5. Why are thereNestedScrollingChild, there areNestedScrollingChild2Those of you who don’t have enough work will notice that Google has recently added oneNestedScrollingChild3This is all going on, huh? What have you changed?

Don’t worry, to explain these problems, you need to take a look at the history of the SDK and the support library:

3.1. First release, September 2014

In Android 5.0 / API 21 (2014.9), Google added NestedScrolling for the first time.

Although not mentioned at all in the version update, you can already see nested sliding-related methods in the View and ViewGroup source code. In addition to ScrollView, there are AbsListView, ActionBarOverlayLayout, and so on, which are basically all the views related to sliding at that time. So, as shown in example of nested ScrollView above, when the Android 5.0 you can actually by setNestedScrollingEnabled (true) switch to enable the View of nested sliding effect.

This is the first implementation of the NestedScrolling mechanism.

3.2. Refactoring first release, April 2015

Because the first version of NestedScrolling was added to the framework’s views and viewgroups, NestedScrolling was available only on Android 5.0, which was the latest version. As we all know, developers don’t like them very much, so NestedScrolling wasn’t used much at the time. (So it’s perfectly normal for people to refer to NestedScrollView when they talk about nested slides, not knowing that ScrollView already has nested slides.)

And Google said, well, you can’t do this, you can’t have nested bugs that don’t work and you have to share good things, so they sort out the features, Two Helper interfaces (NestedScrollingChild and NestedScrollingParent) are reconstructed (NestedScrollingChildHelper, NestedScrollingParentHelper) plus a out-of-the-box NestedScrollView, in Revision 22.1.0 (2015.4), Add them to the V4 Support Library deluxe lunch.

Now everyone is happy to tell you: do you have a nested slider card? Go to NestedScrollView, Android 1.6 can also be used. NestedScrollingChild and NestedScrollingParent are also known, so if you want to nest them, you can implement these two interfaces.

In Revision 22.2.0 (2015.5), Google released the Design Support Library. The killer control for CoordinatorLayout excels in NestedScrolling.

NestedScrolling finally took off.

In the first version, the methods in the View and ViewGroup are split into interfaces and Helper scrolling. In the first version, the methods in the View and ViewGroup are displayed in two parts. The NestedScrolling in the NestedScrollView is the same as that in the ScrollView, so the NestedScrolling mechanism is essentially the first version, but in a different form.

What is the impact of the change in the form of NestedScrolling?

  1. Stripping out NestedScrolling from views and viewgroups, putting apis in interfaces, and implementing them in helpers is what it’s all about
  2. Indeed, since this mechanism doesn’t really involve the core framework layer stuff, having it exist outside of the API version and allowing the experience of nested sliding on lower versions of the system is the main reason for this change and its main advantage. As for dependency inversion, composition over inheritance should be only a consequence. Google, which is easy to fix bugs (x 2), probably didn’t think of that either.
  3. At the same time, there are definitely not only advantages to doing this, but also disadvantages. Otherwise, we would not have directly added the mechanism to the View and ViewGroup in the first place. The main disadvantages are:
    1. Cumbersome to use. This is certain, originally put in the View to use the method, now not only need to implement the interface, but also to write the interface implementation, although there are Helper classes to assist, but still trouble ah
    2. Expose more internal apis that the average user doesn’t care about. This I think is more important than the previous one, because it affects how quickly developers can pick up the whole mechanic. As I mentioned earlier, you only need to know that there are nine methods. Now there are nine child methods and eight parent methods, which is close to double. Many of these methods are for internal communication (e.gisNestedScrollingEnabled(),onNestedScrollAccepted()), some are designed to be awkward and rarely used (e.gdispatchNestedFling()), some of which require special refinement of details (e.ghasNestedScrollingParent()), developers don’t really care at first.

3.2.1. Bugs in the first version

Android 1.6 also uses nested slides, and grandma is grinning from ear to ear. After the novelty wore off, they started to feel dissatisfied, resulting in the famous Bug of the first version of NestedScrolling: “inertia discontinuous” (back to summary).

What is inertia discontinuity? The following figure

To put it simply: when you slide the internal View, quickly lift your finger, the internal View will start to slide inertia, when the internal View inertia slide to its top will stop sliding, at this time the external sliding View will not have any reaction, even if the external View can slide. Originally, this experience is not much problem, but because when you manually slide, when the inside slides to the top, you can then slide the outside View, which forms a contrast, there is a gap, there is a gap, the masses are not satisfied, you can not slide the inside to the outside of the inertial slide, can’t you? So it’s not a Bug, it’s just that the experience isn’t that good.

The Fling API is not without some consideration for inertia, and the four fling apis are not without their bizarre design and usage.

The four apis look like this, so if you look at the names of the four apis corresponding to the scroll, you’ll probably know what they do (but there’s a big difference, see below) :

  1. Ns child:dispatchNestedPreFling,dispatchNestedFling
  2. Ns the parent:onNestedPreFling,onNestedFling

In the previous description, the default is to let NS child directly consume the inertial sliding generated when the user lifts quickly. This is no problem, because we also pass the inertial sliding to THE NS parent in the computeScroll method, so that the parent and child can cooperate to carry out the inertial sliding. But the NestedScrollView actually looks like this:

public boolean onTouchEvent(MotionEvent ev) {...case MotionEvent.ACTION_UP:
        if (mIsBeingDragged) {
            ...
    
            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                flingWithNestedDispatch(-initialVelocity);
            }
    
            stopNestedScroll();
        }
        break; . }private void flingWithNestedDispatch(int velocityY) {
    final int scrollY = getScrollY();
    final boolean canFling = (scrollY > 0 || velocityY > 0) && (scrollY < getScrollRange() || velocityY < 0);
    
    if(! dispatchNestedPreFling(0, velocityY)) {
        dispatchNestedFling(0, velocityY, canFling);
        if(canFling) fling(velocityY); }}public void fling(int velocityY) {
    if (getChildCount() > 0) {... mScroller.fling(getScrollX(), getScrollY(),0, velocityY, 0.0.0, Math.max(0, bottom - height), 0, height/2);
        ViewCompat.postInvalidateOnAnimation(this); }}@Override
public void computeScroll(a) {
    if (mScroller.computeScrollOffset()) {
        ... // There is no logic to distribute slides to ns parent}}Copy the code

Read the logic

  1. Let’s start with the API. Just like sliding, inertia (velocity) has a mechanism for co-consumption, but it’s not quite the same, or completely different
  2. Swipe in userns childAnd quickly lift the finger to generate inertia. LookflingWithNestedDispatch()Method,ns childWill askns parentWhether to consume this speed
    1. If consumption, the speed of all to hand over, no longer their consumption
    2. ifns parentNo consumption, then speed will be handed over againns parent, and tell itself whether it has a consumption speed condition * (according to the system class library always written, ifns childAt this rate of consumption,ns parentWill not do to this speed) *, and their own consumption speed in the condition of consumption speed, on the speed of consumption
  3. The way you consume speed is to use itmScrollerInertial sliding, but in thecomputeScroll()Does not distribute slides tons parent
  4. At the end, as long as you lift your finger, it will callstopNestedScroll()Remove andns parentThis is the end of the collaboration

So to sum up:

  1. The inertia of this collaborative consumption mechanism can only give way before inertia slipsns parentThere is an opportunity to intercept processing inertia, and it does not allow in the process of inertial slidingns childandns parentThe slippage caused by co-consumption inertia is not going to achieve the desired effect of inertia continuity, so it may not be a good idea for the developers of the first version to achieve inertia continuity by directly transferring inertia
    1. In addition, the current inertia of collaborative consumption mechanism will only inns childThere is a certain effect when there is no sliding (although it can be completely replaced by the sliding co-consumption mechanism), and in later versions this effect is not used at all, it is replaced by the sliding co-consumption mechanism
  2. The way to achieve inertia continuity is actually very simple, without adding new mechanisms, directly through the sliding collaborative consumption mechanism, inns childWhen you do an inertial slide, you transfer the slide, and that’s it
  3. The first version of NestedScrolling was fine, but the system controls didn’t use it in the right way
  4. So fixing this Bug is easy, but tedious: change everythingns childFor system controls that use nested sliding mechanisms, inertia-related apis and processing logic can be retained as long as thecomputeScroll()The middle handle slidesdispatchNestedPreScroll()anddispatchNestedScroll()Method distributed tons parent, and then change the release andns parentThe binding time is placed after the fling ends
  5. Your ownns childView can be directly changed, but the system providesNestedScrollView,RecyclerViewControl, you can only put an issue and other official repair, but you can also copy a copy out of their own change

3.3. Second release, September 2017

Google says they don’t want to talk to these people. They just want you to use them. I was still working on my AI until September 2017, more than two years later, when Revision 26.1.0 came up. Updated NestedScrollingChild2 and NestedScrollingParent2, and fixed bugs in the first version of the system controls. This is the second version of NestedScrolling

Take a look at the second version is how to deal with the first version of the Bug, Daniel’s fire thinking is indeed stronger than the average person.

First look at how the interface is changed:

  1. ns childincomputeScrollDistribute the slide tons parentNo problem (that’s the key), but I want to distinguish between sliding triggered by user finger movement and sliding triggered by inertia (that’s the icing on the cake)
  2. So the second edition gives allNestedScrollingChildIn sliding related(Other than the fling related, sliding switch, to be exact)Five methods, allNestedScrollingParentIn sliding related(Not to fling, to be exact)Five methods, each with an additional parametertype.typeThere are two values representing the above two sliding types:TYPE_TOUCH,TYPE_NON_TOUCH
  3. So the second version of the interface does not add or remove any methods, just add one to the ten methodstypeParameter, and make a compatibility to the old interface, so that theirtypeisTYPE_TOUCH

Once you’ve changed the interface, you need to change the code, and the Helper class needs to change first

  1. The first edition ofNestedScrollingChildHelperIt was holding onens parentmNestedScrollingParentTouch, as a binding relation,The second editionOne morens parentmNestedScrollingParentNonTouch, why two rather than one, presumably to avoid too strict requirements on the life cycle of two types of sliding, such as inNestedScrollViewIn the implementation of, is to open firstTYPE_NON_TOUCHType slides and then closesTYPE_TOUCHType of slide if common onens parentThe domain, you can’t do that
  2. NestedScrollingChildHelperThat’s the main thing that I did, and the rest of the changes were just normal transformations after adding parameters,NestedScrollingParentHelperThere are no special changes in the

In the analysis of the bugs in the first version, I said that “there is no problem with the first version of NestedScrolling, but the problem is that the system controls use the mechanism in the wrong way”, so the biggest changes this time are those system controls that use nested sliding mechanism. Let’s use NestedScrollView as an example to see how the system fixes the Bug and suggests how you should now create the NS Child role. The main changes occur in the expected position:

public boolean onTouchEvent(MotionEvent ev) {...case MotionEvent.ACTION_UP:
        if (mIsBeingDragged) {
            ...
    
            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                flingWithNestedDispatch(-initialVelocity);
            }
    
            stopNestedScroll(ViewCompat.TYPE_TOUCH);
        }
        break; . }private void flingWithNestedDispatch(int velocityY) {
    final int scrollY = getScrollY();
    final boolean canFling = (scrollY > 0 || velocityY > 0) && (scrollY < getScrollRange() || velocityY < 0);
    
    if(! dispatchNestedPreFling(0, velocityY)) {
        dispatchNestedFling(0, velocityY, canFling);
        fling(velocityY); / / China}}public void fling(int velocityY) {
    if (getChildCount() > 0) {
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
        
        mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0.0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0.0); 
        
        mLastScrollerY = getScrollY();
        ViewCompat.postInvalidateOnAnimation(this); }}@Override
public void computeScroll(a) {
    if (mScroller.computeScrollOffset()) {
        final int x = mScroller.getCurrX();
        final int y = mScroller.getCurrY();
    
        int dy = y - mLastScrollerY;
    
        // Dispatch up to parent
        if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) {
            dy -= mScrollConsumed[1];
        }
    
        if(dy ! =0) {
            final int range = getScrollRange();
            final int oldScrollY = getScrollY();
    
            overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0.0.false);
    
            final int scrolledDeltaY = getScrollY() - oldScrollY;
            final int unconsumedY = dy - scrolledDeltaY;
    
            if(! dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null, ViewCompat.TYPE_NON_TOUCH)) {
                if (canOverscroll()) showOverScrollEdgeEffect();
            }
        }
    
        ViewCompat.postInvalidateOnAnimation(this);
    } else{ stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); }}Copy the code

The computeScroll() method is more heavily coded because it is not only the main part of this Bug fix, but also the part that will be changed in the next one. But the whole logic is pretty simple, as expected. A brief explanation:

  1. UPTime to do things have not changed, or in this lifted withns parentIs bound, but the type is indicatedTYPE_TOUCH
  2. flingWithNestedDispatch()I’m not going to do that
  3. infling()Method, callstartNestedScroll()A new binding is opened, but the type is changedTYPE_NON_TOUCH
  4. Most of the changes were made incomputeScroll()Method, but the logic is clear: for everydy, will go through the “parent -> child -> parent -> Child” consumption process, so as to achieve inertial continuity, solve the Bug

The end result looks like this:

In addition, since this version, the View and ViewGroup’s NestedScrolling mechanism has not been updated, and remains the same as the first version.

3.3.1. Bugs in version 2

This looks like a nice change to the second version, but it actually introduces two more problems, at least one of which is a Bug, and one of which is just not good enough interaction for a very confusing reason.

Let’s start with the first question: “twice as fast” (back to summary)

  1. All I know is that it just happened to show upNestedScrollView,RecyclerViewI highly doubt it was introduced because of hand slip
  2. It does this: When the outer View is not on top and the inner View is on top, slide the inner View and lift it quickly.
    1. The expected effect should be: the external View slides down inertia
    2. In fact, this is probably the case, but with a slight difference: the external View slides down about twice as fast as you’d expect (and backwards as well), as shown below
  3. Why is that?
    1. If you update the second version of the nested sliding mechanismNestedScrollViewYou can easily see the comparisonflingWithNestedDispatch()(in the code I posted),fling(velocityY)In front of theif (canFling)Mysteriously disappeared
    2. However, disappearance does not mean hand slip, it may be caused by logic, so I sorted out the logic, does this if judgment need to be removed in the new mechanism? Well, not necessarily. Removing the IF allows the external View to hold two flings simultaneously, which is exactly what the actual experience is
  4. So to solve this problem, it’s easy to just fill in the if judgment
  5. However, this problem is not obvious in the experience, but it is not difficult to find, but users may not know whether it is a Bug or a Feature (233)


Then there’s the second problem: “Air motors” (back to summary)

  1. This must be a Bug, all nested sliders exist, and the experience is obvious
  2. This problem is one of the more hardcore, really is NestedScrolling mechanism problem, specifically should call defects, existed in the first edition, only the first edition of system control mechanism of improper use not trigger this problem, but after the second edition, each control switch to the new way of using, this problem was finally exposed
  3. It does this: When the outer View is on top and the inner View is on top, slide the inner View and fling it.(Nothing is going to happen right now because it’s all over the top, the next step is the key)You’ll slipThe View outside
    1. The expectation should be: External View scrolling up
    2. In fact, you’ll find that you can’t move it, or that you slide up a bit and come back down again, as if an invisible motor is competing with your finger (and the other way around), as shown in the image above
  4. Why is that?
    1. In fact, I had no idea at the beginning, so I had to log to see who was the motor. After debugging for a while * (I also made a joke at that time) * I found that the motor was the internal View
    2. The explanation is simple enough:
      1. Let’s go back to the methodflingWithNestedDispatch()The code in thedispatchNestedPreFling()Most of the time it comes backfalseIn almost all cases, the internal View will passfling()Method to start yourselfmScrollerThis little motor
      2. And then after the little motor starts, tocomputeScroll()Method, you’ll see, (if you don’t touch the inner View directly)There is no external force to stop the motor unless it stops itselfSo it’s going to keep going outdispatchNestedPreScroll()anddispatchNestedScroll()
      3. So in the above phenomenon, even though both the inside and outside views are on top, they can’t slide, but the inner View’s little motor is still chugging, as soon as you slide the outside View to a position not on top, it will slide it down again
      4. So you don’t need the “when the outer View is at the top and the inner View is at the top” scenario (this is just the best scenario to reproduce), you can see this problem when you turn on the inner View’s motor in any way, but you don’t close it by touching the inner View directly
  5. So what? What’s the crux of the problem?
    1. First of all, the internal View’s small motor can not be abandoned, without it, how to chug chug to drive the external View?
    2. But don’t let it turn TuTuTu incessantly, in addition to the user directly touch the internal View to make it stop, it also needs to have a stop switch, at least let the user touches the external View can also shut it down, the more reasonable implementation process should also let the driver to feedback, when a situation cannot drive (for example, both inside and outside slide to the top), stop the motor
  6. So now you need to add feedback to the driver process
    1. Above mentionedIn this mechanismns childBe the one who takes the initiative,ns parentIt’s completely passive,ns parentUnable to initiate notificationns childOh, I’m stuck. Oh, I hit the wall
    2. butns parentIt’s not impossible to tellns childInformation, through the return value of the method and the parameter of the reference type,ns childStill available fromns parentTo obtain information from
    3. So just add a set of methods to the NestedScrolling mechanism so thatns childaskns parentSliding or not, the problem should be solved: ifns parentI can’t slide,ns childYou can’t slide yourself, so just shut down the motor,Saving energy is everyone’s responsibility
  7. I think so, but I can’t have G’s food. You didn’t write the NestedScrolling mechanism, how can you add a method to the whole mechanism? Okay, so what’s the backdoor to the NestedScrolling mechanism
    1. If you try it, it might work. Askns parentIsn’t there a way to slide?
    2. dispatchNestedPreScroll()Will letns parentinns childBefore sliding, and the sliding distance is recorded in its array parameterconsumedGet the value in the arrayns childYou can knowns parentWhether it slides at this point
    3. dispatchNestedScroll()Will makens parentinns childAfter the slide, it has no array parameter to record the slide distance, it only has a return value to record whether the slide consumed… No, this return value is not used to record whether to consume a swipe, it isns parentIf you can make contact, if so, returntrueAnd doesn’t care if it consumes a swipe. inNestedScrollingChild HelperYou can also see a clear implementation of this logic in theNestedScrollingParent2Its corresponding method invoid onNestedScroll(), there is no return value *dispatchNestedScroll()In theint[] offsetInWindowAn array location that is not used to pass information, and the result is that the corresponding method in parent does not take this parameter. andns parentNor can they relieve themselves of their ownns childThis road is also blocked) *. In a word,dispatchNestedScroll()Can’t getns childLearned thatns parentThe consumption of the event is blocked
    4. (Actually through the laterdispatchNestedScroll()The consumption results are directly placed onns childIn View, this backdoor is used to solve the Bug, but the limitations of this method are relatively large, and the latest version of the third version has been fixed, I will not write more.)

3.4. Third release, November 2018

The second version is more buggy than the first version, but not as many people seem to know about it, and perhaps not as many usage scenarios. After more than a year, However, Google has finally recognized the problem. In the latest update to Androidx.core 1.1.0-Alpha01, released on November 5, 2018, The latest fixes — NestedScrollingChild3 and NestedScrollingParent3 — have been given, as have a number of system components.

This is the third version of the NestedScrolling mechanism. This version does deal with the above two bugs, but unfortunately the second Bug is not completely fixed. Looking forward to version 4) (As this article nears completion, the new firefighter tweeted on 12/3/2018 that version 3 has been released, Results comments section you have been happy to expect NestedScrollingChild42 NestedScrollingChildX NestedScrollingParentXSMax NestedScrollingParentFinalFinalFinal NestedScrollingParent2019)

How does the boss put out a fire in this version

As usual, first look at the interface, a look at the interface changes you may laugh, is really where impassability change where

  1. At the interfaceNestedScrollingChild3In, there is no increase method, just givedispatchNestedScrollMethod adds a parameterint[] consumedAnd put itbooleanThe return value is changedvoidWith access to more detailed information there is no need for thisbooleanthe
  2. interfaceNestedScrollingParent3Again, just a change of method. HereonNestedScrollincreasedint[] consumedArgument (which returns the value ofvoid, did not change)

Here is the comparison in NestedScrollingChild3:

/ / 2
boolean dispatchNestedScroll(
    int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed, 
    @Nullable int[] offsetInWindow,
    @NestedScrollType int type
);
    
/ / 3
void dispatchNestedScroll(
    int dxConsumed, int dyConsumed, 
    int dxUnconsumed, int dyUnconsumed,
    @Nullable int[] offsetInWindow, 
    @NestedScrollType int type,
    @NonNull int[] consumed //;
Copy the code

Again see a Helper, NestedScrollingChildHelper basic no change, in addition to fit the new interface NestedScrollingParentHelper is strengthened a little logic rigor (is review about 233)

In the end, as our old friend NestedScrollView shows, the changes are basically the same as expected:

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
            
    final int oldScrollY = getScrollY();
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;
    
    if(consumed ! =null) consumed[1] += myConsumed; // Just add this sentence
    
    final int myUnconsumed = dyUnconsumed - myConsumed;
    mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
    
// ---
    
// The logic in onTouchEvent has not changed
private void flingWithNestedDispatch(int velocityY) {
    if(! dispatchNestedPreFling(0, velocityY)) {
        dispatchNestedFling(0, velocityY, true);
        fling(velocityY); // The logic in the fling remains unchanged}}@Override
public void computeScroll(a) {
    if (mScroller.isFinished()) return;
    mScroller.computeScrollOffset();
    final int y = mScroller.getCurrY();
    
    int unconsumed = y - mLastScrollerY;
    
    // Nested Scrolling Pre Pass
    mScrollConsumed[1] = 0;
    dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH);
    unconsumed -= mScrollConsumed[1];
    
    final int range = getScrollRange();
    
    if(unconsumed ! =0) {
        // Internal Scroll
        final int oldScrollY = getScrollY();
        overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0.0.false);
        final int scrolledByMe = getScrollY() - oldScrollY;
        unconsumed -= scrolledByMe;
    
        // Nested Scrolling Post Pass
        mScrollConsumed[1] = 0;
        dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
        unconsumed -= mScrollConsumed[1];
    }
    
    // Finally, there is the unconsumed case
    if(unconsumed ! =0) {
        if (canOverscroll()) showOverScrollEdgeEffect();
    
        mScroller.abortAnimation(); // Close the small motor
        stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
    }
    
    if(! mScroller.isFinished()) ViewCompat.postInvalidateOnAnimation(this);
}
Copy the code

ComputeScroll () has the most changes, but there are some other changes as well.

  1. becauseonNestedScroll()Added a parameter to record distance consumption, sons parentYou need to record this data and pass it on to yourselfns parent
  2. flingWithNestedDispatch()It is the method with honey Bug before, originally I expected to restore the first version of writing, that is, thefling(velocityY)In front of theif (canFling)Add it back, and it turns out, evencanFlingI don’t judge,dispatchNestedFling(0, velocityY, true)Direct transfertrue.fling(velocityY)Always call. What does that mean? You have to think about it in terms of the way most views are written
    1. searchAPI 28You’ll see the following code:
      1. foronNestedPreFling()Methods in addition toResolverDrawerLayoutIt will consume the fling and return in some casestrue, as well asCoordinatorLayoutI’ll ask my kids a token questionBehaviorThe other way you write it, you just return itfalse
      2. foronNestedFling(boolean consumed)Methods, all of them, as long asconsumedfortrueYou don’t do anything, which is perfectly natural, right
    2. So the current situation is that in most cases, the inner View’s fling starts, and the outer View does not consume the inner View’s fling. This means that inertial collaboration has been completely replaced by sliding collaboration. This is why I don’t recommend this set of useless interfaces to beginners, right
    3. But of course you can use fling if you have a special need to do so
  3. The last termcomputeScroll(), it basically implements the idea that we discussed when we were discussing how to fix bugs in the second releasedispatchNestedPreScroll()anddispatchNestedScroll()Learned thatns parentHow much the distribution of the sliding distance, but also have their own consumption, a total of the two, if there is no consumption of sliding distance, it must be both inside and outside are sliding to the end, so it is decisive to shut down the small horse

This is what it looks like now, and you can see that the bugs in version 2 have actually been fixed

3.4.1. Bugs in version 3

So why did I say that the second Bug was not solved completely?

  1. Comparison code is easy to see in version 3DOWNThe handling of events is the same as in version 2. There is no mechanism for touching the external View to turn off the internal View motor. More specifically, there is no mechanism for touching the external View to prevent consumption of the sliding passed by the internal View.
  2. So the motor can only be turned off when the external View slides to the end, and the external View cannot tell the internal View that it is pressed

Although the phenomenon is similar to that of the “air motor”, it is customary to give it a pleasant new name:… “Can’t hold it down” (Back to summary)

The actual experience is the same as the analysis results. In this way, when the external View is triggered by sliding the inner View, you can’t stop it by touching the external View. The external View is easier to reproduce when it is longer, as shown below (changed direction).

However, only the ns parent that can respond to touch needs to be considered. The NS parent that can respond to touch is mainly NestedScrollView, so this problem is mainly the problem of NestedScrollView. In addition, it has nothing to do with the mechanism, but NestedScrollView is not used correctly, so it is unlikely that there will be a fourth NestedScrolling mechanism. Probably just a generic update to NestedScrollView.

This problem is also very easy to fix, just need to give the NS child feedback after the DOWN event that it was pressed, you can use reflection, or directly move the NestedScrollView out to fix, the key code is as follows

private boolean mIsBeingTouched = false;

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            mIsBeingTouched = true;
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingTouched = false;
            break;
    }

    return super.onTouchEvent(ev);
}

private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
    final int oldScrollY = getScrollY();
    if(! mIsBeingTouched) scrollBy(0, dyUnconsumed); // Just change this sentence
    final int myConsumed = getScrollY() - oldScrollY;

    if(consumed ! =null) {
        consumed[1] += myConsumed;
    }
    final int myUnconsumed = dyUnconsumed - myConsumed;

    childHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
Copy the code

I have changed it with reflection and put it here. You can also use it directly. The effect is as follows:

3.5 summary

The history is finally done, summary (go back to the detailed history)

  1. In September 2014, Google launched theThe Android 5.0 (API)The first version of NestedScrolling was added to views and viewgroups in theScrollViewThere are no interaction issues, but this mechanism can only be used at API 21 or above
  2. In April 2015, Google reconstructed the first version of NestedScrolling, leaving the logic unchanged but stripping it out of views and viewgroups to create two interfaces.NestedScrollingChild,NestedScrollingParent) and two helpers (NestedScrollingChildHelper,NestedScrollingParentHelper), and uses this new mechanism to rewrite a default enabling nested slidingNestedScrollViewAnd put them all inRevision 22.1.0thev4 support libraryThe nested sliding mechanism can be used on earlier versions of the system, but the first version has it”Inertia discontinuity”The Bug
  3. In September 2017, Google launched theRevision 26.1.0thev4 support libraryReleased the second version of NestedScrolling, adding interfacesNestedScrollingChild2,NestedScrollingParent2, mainly adds a parameter to the original sliding correlation methodtypeRepresents two types of slidingTYPE_TOUCH,TYPE_NON_TOUCH. And a new mechanism is used to override nested sliding-related controls. This update addresses the “inertia discontinuity” Bug in the first release, but also introduces a new Bug:”Double speed”(onlyNestedScrollView) and”Air motor”
  4. In November 2018, Google was mergedAndroidXThe family’s NestedScrolling mechanism has been updated for the third versionAndroidx. Core 1.1.0 - alpha01, added an interfaceNestedScrollingChild3,NestedScrollingParent3The change is only for the originaldispatchNestedScroll()andonNestedScroll()increasedint[] consumedParameters. And then the nested sliding-related controls were rewritten with a new mechanism. This update addresses the second versionNestedScrollView“Double speed” Bug, and hope to solve the “air motor” Bug, but not completely solved, still left”Can’t hold it down”Bug

So you should have the answer to the previous question:

  1. Using interfaces and helpers for low version compatibility and easy escalation is not the easiest way to use NestedScrolling. So I’m just going to say call the View and ViewGroup methods for simplicity, but the best way to do that is to implement the latest interface with Helper and then call the methods that you implement, Because View and ViewGroup methods have high API version requirements, their own version is very low. This is an easy change to use, because the method name is the same as in View and ViewGroup, and Helper use is straightforward, so I won’t give you any examples.
  2. There are 9 ways to fling and not to fling. There are 8 ways to fling and not to fling. However, the second and third versions of the mechanics did not add new methods, and the overall design of the mechanics did not change significantly.
  3. The second and third editions were Bug fixesWell, I’m not finished yet.

(Please indicate the author: RubiTree, address:blog.rubitree.com )

Practice of 4.

Section 2 actually covers this in practice and provides a template for implementing NS Child. Here I’m going to use a more practical example I just found to illustrate the implementation of NS parent and a few details of ns Child in the system library.

4.1. Topic Selection: Hover layout

This example is a “hover layout”. You can call it a sticky layout, a floating layout, a folded layout, but it should look something like this:

The text description is as follows:

  1. Page content is divided into Header, hover area (usually TabLayout) and content area, which can slide left and right, there are multiple Tab pages, and each Tab page is allowed to slide up and down
  2. When the user slides up, the Header is folded first. After the Header is folded and folded, the hover area stays still and the content area slides up
  3. When the user slides down, the content area slides down, then expands the Header, and the hover area moves down
  4. The sliding of the content area and the folding and expansion of the Header should be continuous when the user slides continuously, and even when the user lifts quickly in the sliding process, the sliding inertia also needs to be continuous between the two actions

At this point in time (2019.1.13), this example has a lot of practical significance, because although it is a relatively common interaction effect, but the mainstream APP in the market is actually like this… (Hungry Me v8.9.3)

Regardless of whether they are implemented in Native or not, just look at the effect of implementation

  1. Bilibilii’s video details page and Meituan (no stickers) are the best. They have continuous sliding inertia, but there is a small flaw: you can slide left and right while swiping up and down in the Header, making it easy to misoperate
  2. The problem of Tencent classroom is the most common: inertia discontinuity
  3. The weirdest thing is the store homepage of Ele. me and the Live details page of Zhihu. Both are revenue-generating pages

There are also some weird bugs that I won’t mention. So let’s see if this feature is really that hard to implement.

4.2. Demand analysis

If the content area only has a Tab page, a simple and direct implementation idea is: the page is a sliding control, the hovering area will constantly adjust its position in the sliding process, to achieve the effect of hovering. Its implementation is very simple, the effect is also fully meet the requirements, not for example, you can try.

But the need here is to have a number of Tab pages, it can not be realized with a whole sliding control idea, need to use a number of sliding controls to achieve

  1. Take a look at the sliders: There must be separate sliders within each Tab page. To expand and collapse the Header, you can use the entire container as a single slider
  2. This turns out to be an external slide that works with a set of internal slides. It looks a little complicated, but in fact there is only one external slide that works with one internal slide during a user slide
  3. The matching process looks like this (you can look back at the previous dynamic diagram of the desired effect) :
    1. The user slides up, the external slide control first consumes the event to slide up, until the slide to the bottom of the Header, the external slide control slides over, the slide event to the internal slide control, the internal slide control continues to slide
    2. The user slides, the internal slide control first consumption events slide, until the slide to the top of the internal control, the internal slide control slide end, the slide event to the external slide control, the external slide control continue to slide
    3. When the user lifts quickly in the process of sliding inertia sliding, also need to follow the above matching law

Before you know the mechanism of NestedScrolling, you may feel that this requirement is not right. Indeed, from a large perspective, a single touch of the user causes multiple views to consume it, which violates the principle of event distribution and exceeds the functionality provided by the Android Touch event processing framework: If the parent View doesn’t run out of events, the child View will continue to be used

But specific to this requirement

  1. First, the desired effect of two sliders in combination with a consumption event is to give the user the perception that they are sliding an entire control, except for parts that hover, in the same way that the content area has a single Tab page. It does not go against the user’s intuition. So, with careful design, it can be intuitive for multiple Views to consume the same event stream. One of the most prominent examples in this area isCoordinatorLayoutIt is designed to help developers achieve the effect of their carefully designed multiple Views consuming the same event stream
  2. Then, because of the simplicity of sliding feedback, it is possible to make multiple sliding controls work together. You can do it yourself, or you can use something that we’re already familiar withNestedScrollingMechanism implementation. In additionCoordinatorLayoutIt is also useful to have multiple sliders working together to consume the same event streamNestedScrollingmechanism

OK, now that the requirements are good and we can implement them, let’s see how to implement them.

I know, I know, CoordinatorLayout! Yeah, the most common way to do this right now is to use AppBarLayout, which is based on CoordinatorLayout, which is kind of a natural effect, you can do it with a simple configuration, and you have a lot of other effects, which is really cool, The bilibili video details page is used to achieve the better effect seen in front of it. AppBarLayout achieves this function by using the NestedScrolling mechanism provided by CoordinatorLayout (although the specific method is slightly different from the above analysis, but it is not important. If you’re not comfortable with AppBarLayout and want to hover alone, you can use NestedScrolling.

Use NestedScrolling to create a normal hover layout like Bilibili-bili-bole.

4.3. Requirement realization

After using the NestedScrolling mechanism, you will find that it is very simple to implement. The above analysis process has corresponding interfaces directly in the mechanism. We just need to implement a NS parent that meets the requirements. The NestedScrolling mechanism automatically manages the binding of ns parent and NS child and scroll passing, even if the NS child and NS parent are separated by several views.

The ns parent I want to implement is called SuspendedLayout, and the key code is as follows. The rest of the code and the layout and page code can be viewed here (simply use the first child View as a Header, The second child View will hover naturally.

override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int, consumed: IntArray) {
    if (dyUnconsumed < 0) scrollDown(dyUnconsumed, consumed)
}

override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
    if (dy > 0) scrollUp(dy, consumed)
}

/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /

private fun scrollDown(dyUnconsumed: Int, consumed: IntArray?). {
    val oldScrollY = scrollY
    scrollBy(0, dyUnconsumed)
    val myConsumed = scrollY - oldScrollY

    if(consumed ! =null) {
        consumed[1] += myConsumed
    }
}

private fun scrollUp(dy: Int, consumed: IntArray) {
    val oldScrollY = scrollY
    scrollBy(0, dy)
    consumed[1] = scrollY - oldScrollY
}

override fun scrollTo(x: Int, y: Int) {
    val validY = MathUtils.clamp(y, 0, headerHeight)
    super.scrollTo(x, validY)
}
Copy the code

So fast, the effect is perfect, almost the same as Bilibili:

4.4. Optimize misoperation problems

But the effect is as good as it is bad, and bilibilii’s easy misoperation problem is also here. Why is this a problem?

  1. It’s easy to tell from the appearance of the problem that it must have been during the ascentViewPagerIntercept the event, which isns childDid not “request external do not intercept event flow” in time, so toNestScrollViewRecyclerViewIn view, the problem is actually in the previous descriptionns childonTouchEvent()The logic in theon
  2. becausens childWill determine the user after sliding “request external does not intercept the event stream”, butonTouchEvent()In the judgment of the user in the slide before the slide withdispatchNestedPreScroll()The method is passed tons parentAnd you can see that I’m already recognized as sliding up and downns child“, and had been sliding a distance, will suddenly switch to slidingViewPager

So how do you fix this?

  1. Modifying the source code directly is definitely the solution
    1. I triedNestScrollViewCopy the code and put it indispatchNestedPreScroll()Method is called after the slide has been determined, which does solve the problem
  2. But what about not copying the source code?
    1. It’s ok, as long as it’s called in timeparent.requestDisallowInterceptTouchEvent(true)Can,The complete code is here, the key codes are as follows:
private int downScreenOffset = 0;
private int[] offsetInWindow = new int[2];

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
        downScreenOffset = getOffsetY();
    }

    if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
        final int activePointerIndex = ev.findPointerIndex(getInt("mActivePointerId"));
        if(activePointerIndex ! = -1) {
            final int y = (int) ev.getY(activePointerIndex);
            int mLastMotionY = getInt("mLastMotionY");
            int deltaY = mLastMotionY - y - (getOffsetY() - downScreenOffset);

            if(! getBoolean("mIsBeingDragged") && Math.abs(deltaY) > getInt("mTouchSlop")) {
                final ViewParent parent = getParent();
                if(parent ! =null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
                setBoolean("mIsBeingDragged".true); }}}return super.onTouchEvent(ev);
}

private int getOffsetY(a) {
    getLocationInWindow(offsetInWindow);
    return offsetInWindow[1];
}
Copy the code

Here’s a detail worth mentioning: Instead of just using mlastmotiony-y, subtract (getOffsetY() -downscreenoffset), The offsetInWindow also appears in the dispatchNestedScroll() interface in the NestedScrolling mechanism

  1. offsetInWindowIs critical because whenns childdrivens parentThe slide,ns childIt’s actually moving, right nowns childTriggered by the finger obtained frommotion eventIn thexandyValue is relativens childSo if you just useyValue, you’ll findyThe value has barely changed, and that’s how it works outdeltaYIt’s not going to change, so I need to get it againns childThe offset relative to the window, and we count itdeltaYTo get what you really needdeltaY
  2. ViewPagerWhy would it be able to intercept a sideship after a vertical slide that far, and that’s why it picked it updeltaYIn fact very small

After the change, the effect is as follows, you can see that the problem is solved:

RecyclerView and other NS children can also make similar changes if needed (however, the reflection code here has some impact on performance, it is suggested to make some optimization on implementation)

(Please indicate the author: RubiTree, address:blog.rubitree.com )

5. To summarize

If you did not skip here, I believe you have a clear understanding of the NestedScrolling mechanism, whether it is using, principle or even gossip history, otherwise I can only doubt your expression level of my Chinese teacher.

As for the design of the code, you can probably learn a little bit. You should be impressed by the Google engineers who bravely put out the fire three times.

One last word about usage:

  1. If you need the best nested sliding experience yet, either with system View or custom View, go straight to the latest AndroidX and use series 3 for customization
  2. If your project can’t switch from AndroidX at the moment, upgrade to the latest version of v4 and use series 2 for customization
  3. If your project is for extreme experience and you happen to use nestedNestedScrollViewThinking that version 3 bugs will also affect your valuable and sensitive users, try implementationMy project 😀

At the end of the day, even the firefighters in the G family have trouble, let alone the chicken. This article is sure to have some omissions and improper places, welcome to mention the issue

(If you think your writing is good, please give it a thumbs up and go on.)