There is a bug that has been checked for a few days recently, that is, there is a old page that uses NestedScrollView+ TAB + multi-fragment structure due to business complexity (each Fragment has a RecyclerView, that is, there is nested sliding). There was an occasional slippage problem. After r a long time searching the business-related components with no idea, Hardy started the all-purpose component elimination method, which was a binary process of sorting dozens of change components in a batch (yes, it was so dirty). Finally, he located a Flutter component and simply backed it back.

My page is native. Can the Flutter affect my page? I found out that the upgrade of Flutter was caused by 1.17!

This article contains about 3300 words and takes about 9 minutes to read. If individual big picture is fuzzy, can go to personal site to read.

What magic does Flutter 1.17 have

Flutter1.17 is a milestone, did a lot of performance, function, optimization on tool, as shown in the Flutter | 2020 released the first stable version 1.17, there’s such a words:

If your target platform is Android, you will notice that the AndroidX option is now available only when creating new Flutter projects. The AndroidX library provides advanced Android features called Android Jetpack. In the last release, we dropped the original Android Support Library and made AndroidX the default option for all new projects. In Flutter 1.17, the Flutter create command has only one option –androidx. Although existing Flutter applications that do not use AndroidX can still compile, it is time to migrate to AndroidX.

There is no official mention of the androidx version. After upgrading the Flutter to 1.17, we found that there are two core dependencies in the External Libraries.

./gradlew app:dependencies

The first jar is for Java classes, and the next three jars are for SO libraries that depend on each CPU architecture.

As you can see from the first JAR package, this is the pan for passing dependencies! Android x. Core has changed from 1.0.0 to 1.1.0. If you look at the core release, there is a line in the 1.1.0 change:

Added nested scrolling improvements; See NestedScrollingChild3 and NestedScrollingParent3.

NestedScrollView (NestedScrollView)

1.0.0:

class NestedScrollView extends FrameLayout implements
    NestedScrollingParent2.NestedScrollingChild2.ScrollingView{}
Copy the code

1.1.0:

class NestedScrollView extends FrameLayout implements 
    NestedScrollingParent3.NestedScrollingChild3.ScrollingView {}
Copy the code

As you can see, two interfaces have changed from V2 to V3, and the implementation of the NestedScrollView class itself has also been changed.

Pass-through dependencies: exclude a bug that has been around for a few days.

compile('xxx') {
    exclude(group: 'androidx.fragment')
    exclude(group: 'androidx.lifecycle')
    exclude(group: 'androidx.annotation')}Copy the code

Flutter1.17 (flutter_embedding_release-1.0.0-$hash) uses the new code in AndroidX1.1.0. What are the potential risks of this forced downgrading of 1.0.0? We’ll talk about that later.

Or, why not change the business code and actually fix the bug? First of all, there may be more than one business using the nesting slide scene, my page has been fixed, there may be other bugs that haven’t been found. Second, it is a high-risk thing to accept the update of AndroidX just to upgrade the Flutter. Who knows when it will be upgraded to a higher version? So.. That’s right, Hardy dumped the pot, and he did it right!

Whether the downgrade has potential risks

Flutter_embedding_release -1.0.0-$hash jar: flutter_embedding_release-1.0.0-$hash jar: flutter_embedding_release-1.0.0-$hash jar: flutter_embedding_release-1.0.0-$hash jar: flutter_embedding_release-1.0.0-$hash jar: flutter_embedding_release-1.0.0-$hash jar: flutter_embedding_release-1.0.0-$hash jar:

So visible fragments, Lifecycle, annotations do get used, annotations we don’t have to worry about, focus on the other two,

Lifecycle:

Fragments:

Looking at lifecycle changes, it looks like something will be removed and ViewModel functionality will be added, so going down to 1.0.0 doesn’t matter;

The FragmentFactory, ViewModel’s Kotlin delegate, maximum lifetime, FragmentActivity LayoutId constructor, etc. There’s no use for these new things, so for now, androidx’s forced downgrade to 1.0.0 looks safe (and better yet, if enough people are invested and verified).

NestedScrollView

Introduces a

NestedScrollView (1.1.0) NestedScrollView (1.1.0)

Analyze the 1.0.0 version first, and then look at the changes in 1.1.0. NestedScrollView inherits FrameLayout and implements NestedScrollingParent2, NestedScrollingChild2, ScrollingView interfaces. Hold NestedScrollingParentHelper and NestedScrollingChildHelper two helper classes to deal with logic.

Look at the source code is easy to lose hair, or simply use the first feel.

The code is for demonstration only and nesting of NestedScrollView and RecyclerView is not recommended if it is not necessary.

Compared with NestedScrollView, RecyclerView only realizes NestedScrollingChild2, which can only exist as a child layout in the nested sliding system, so RecyclerView is the child and NestedScrollView is the parent.

The layout is simple, just a header and RecyclerView:

<MyNestedScrollView 
      android:id="@+id/nsv_out">

    <LinearLayout
           android:orientation="vertical">

        <ImageView
              android:id="@+id/iv_header"
              android:src="@mipmap/ic_launcher" />

        <MyRecyclerView
              android:id="@+id/rv_list"
              android:layout_width="match_parent"
              android:layout_height="600dp" />

    </LinearLayout>

</MyNestedScrollView>
Copy the code

The height of RecyclerView was specified to ensure normal reuse. Let’s add some logs and observe the nested sliding mechanism. MyNestedScrollView:

class MyNestedScrollView extends NestedScrollView {
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
        // Slide up. If getScrollY is not the height of the header, slide yourself first to hide the header
        boolean hideHeader = dy > 0 && getScrollY() < mHeaderHeight;
        // Slide, if the RV has slid to the top, slide yourself, showing the header
        boolean showHeader = dy < 0 && getScrollY() > 0 && !target.canScrollVertically(-1);
        if (hideHeader || showHeader) {
            scrollBy(0, dy);
            // Tell the RV that I have adjusted the dy distance
            consumed[1] = dy;
            HLog.e("Nested slide", mName + ": Header visible, I'll slide it, I'll slide it for you later."); }}@Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 
                               int dxUnconsumed, int dyUnconsumed, int type) {
        if (0! = dyUnconsumed) { HLog.e("Nested slide", mName + ": You still have" + dyUnconsumed + "No consumption. I don't need it.");
        }
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); }}Copy the code

MyRecyclerView:

class MyRecyclerView extends RecyclerView {
    @Override
    public boolean startNestedScroll(int axes, int type) {
        HLog.e("Nested slide", mName + "Do you want to slide or not?");
        return super.startNestedScroll(axes, type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
        if (0! = consumed[1]) {
            mNsvConsume += consumed[1];
            HLog.e("Nested slide", mName + ": OK, I watched you skate and how much you skied." + mNsvConsume);
        }
        return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                                        int[] offsetInWindow, int type) {
        HLog.e("Nested slide", mName + "So I swipe, I consume." + dyConsumed+"And"+dyUnconsumed+"No consumption. See if you need it.");
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

    @Override
    public void stopNestedScroll(int type) {
        HLog.e("Nested slide", mName + ": End of slide");
        super.stopNestedScroll(type); }}Copy the code

Run as follows:

General process:

As we all know, event distribution has interrupt problems, nested sliding mechanism can be solved, let’s analyze the source code.

RecyclerView as the starting point, startNestedScroll will be adjusted twice, once in onInterceptTouchEvent and once in onTouchEvent. MyRecyclerView (RV) MyNestedScrollView (NSV)

//RecyclerView.java
boolean onInterceptTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_DOWN: / / the down event
            // Vertical axis, touch in (non-inertial)
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            break;
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}

boolean onTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            // Vertical axis, touch
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        } break;
    }
    return true;
}

boolean startNestedScroll(int axes, int type) {
    // callback NSV onNestedScrollAccepted
    return getScrollingChildHelper().startNestedScroll(axes, type);
}
Copy the code

Follow up startNestedScroll,

//NestedScrollingChildHelper.java
boolean startNestedScroll(int axes, int type) {
    if (isNestedScrollingEnabled()) { // Supports nested sliding
        ViewParent p = mView.getParent();
        View child = mView;
        while(p ! =null) { // Go up to NSV
            // Call onStartNestedScroll to see if nested scroll is supported
            //return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) ! = 0;
            NSV supports vertical sliding and returns true
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                // callback NSV's onNestedScrollAccepted
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceofView) { child = (View) p; } p = p.getParent(); }}return false;
}
Copy the code

And then dispatchNestedPreScroll and onNestedPreScroll,

//RecyclerView.java
boolean onTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_MOVE: { / / move events
            // Distribute preprocessing
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
            }
        } break;
    }
    return true;
}

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

Follow up dispatchNestedPreScroll,

//NestedScrollingChildHelper.java
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,
                                int[] offsetInWindow, int type) {
    / / find the NSV
    ViewParent parent = getNestedScrollingParentForType(type);
    // The onNestedPreScroll of NSV is called back, while the RV is passed in as target
    ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
}
Copy the code

And then dispatchNestedScroll,

//RecyclerView.java
boolean onTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_MOVE: { / / move events
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                // Do some calculations...
                / / scrollByInternal
                if (scrollByInternal(
                    canScrollHorizontally ? dx : 0,
                    canScrollVertically ? dy : 0,
                    vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true); }}}break;
    }
    return true;
}

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    // Do some calculations...
    // Call dispatchNestedScroll dispatch, which will callback the NSV onNestedScroll
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, 
                             unconsumedY, mScrollOffset,TYPE_TOUCH)) {
    }
}
Copy the code

And finally, stopNestedScroll,

//RecyclerView.java
boolean onTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_UP: { / / up event
            resetTouch();
        } break;
    }
    return true;
}

void resetTouch(a) {
    // Finally callback NSV onStopNestedScroll
    stopNestedScroll(TYPE_TOUCH);
}
Copy the code

All right, just to clear my head,

  1. Rv in the down event of onTouch, enable nested sliding, startNestedScroll, first adjust the onStartNestedScroll of the parent view to see if it supports nested sliding, then find NSV layer by layer, Callback NSV’s onNestedScrollAccepted
  2. Rv Move event in onTouch, start preprocessing, dispatchNestedPreScroll, callback NSV onNestedPreScroll
  3. Rv Move event on onTouch, dispatchNestedScroll, callback NSV onNestedScroll
  4. Rv Up event in onTouch, end distribution, stopNestedScroll, callback NSV onStopNestedScroll

Thus, RV, as a son, is the active party. At the same time, the unConsumed value is introduced to transfer the remaining distance to each other. The remaining distance that the RV has not consumed can also be handed over to NSV for continued consumption.

V3 Changes

In 1.1.0, the interface implemented by NestedScrollView was changed from v2 to V3, and the v3 interface added a method,

interface NestedScrollingChild3 extends NestedScrollingChild2 {
    // extend 1 method of v2, but int[] consumed
    void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                              int[] offsetInWindow, int type,int[] consumed);
}

interface NestedScrollingParent3 extends NestedScrollingParent2 {
    // extend 1 method of v2, but int[] consumed
    void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                        int dyUnconsumed, int type, int[] consumed);
}
Copy the code

Then we run the demo we just wrote again, but this time we slide the finger down (pull it down) so that the RV has unconsumed distance,

NSV can normally receive rv unconsumed distance,

AndroidX1.1.0 log: NSV did not receive rv unconsumed distance (callback not executed)

So, the old dispatchNestedScroll is still called normally, but the old onNestedScroll is not called normally, is it changed to a new method? Let’s solve the mystery

Why can dispatchNestedScroll be called normally? The previous analysis, he was changed in RecyclerView, of course not affected. Follow up dispatchNestedScroll,

//NestedScrollingChildHelper.java
boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
                                     int dxUnconsumed, int dyUnconsumed,int[] offsetInWindow,
                                     int type, int[] consumed) {
    // Compatible processing classes
    ViewParentCompat.onNestedScroll(parent, mView,dxConsumed, dyConsumed, 
                                    dxUnconsumed, dyUnconsumed, type, consumed);
}

//ViewParentCompat.java
static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
                           int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
                           int[] consumed) {
    if (parent instanceof NestedScrollingParent3) {
        / / callback v3
        ((NestedScrollingParent3) parent).onNestedScroll(xxx);
    } else {
        if (parent instanceof NestedScrollingParent2) {
            / / callback v2
            ((NestedScrollingParent2) parent).onNestedScroll(xxx);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Below v2, only touch TYPE_TOUCH is supported, inertial TYPE_NON_TOUCH is not supported
            if (Build.VERSION.SDK_INT >= 21) {
                // As of 5.0, onNestedScroll is added to the ViewParent interface
                parent.onNestedScroll(xxx);
            } else if (parent instanceof NestedScrollingParent) {
                // Below 5.0, callback v1((NestedScrollingParent) parent).onNestedScroll(xxx); }}}}Copy the code

SDK21 started to support nested sliding, adding nest methods directly to views and ViewGroups, but for forward compatibility, The two interfaces NestedScrollingChild and NestedScrollingParent are provided in the Android.support. v4 package. To achieve nested sliding, you can either use SDK21 View or implement the two interfaces yourself. Going back to AndroidX, using the xxxParent interface as an example (xxxChild is similar),

  1. NestedScrollingParent: Defines nest methods
  2. NestedScrollingParent2: Extends these nest methods by adding the type parameter to indicate yesTouch the slidingorInertial slidingfling
  3. NestedScrollingParent3: Extends 1 Nest method onNestedScroll and adds 1 parameter int[] consumed

Google has made a good compatibility process, but because the demo I wrote is inherited from NestedScrollView, the interface of NestedScrollView is automatically changed into V3 with the upgrade of AndroidX, and the v3 condition is matched when calling back onNestedScroll. The callback that takes the most parameters onNestedScroll (the old callback doesn’t take any parameters), so the demo code will crash (hardy’s actual problem is not this, the demo is just for demonstration).

The end of the

Just, uh, two things,

  1. Note the problems with transitive dependencies. Blocking dependencies may cause classes to be lost, but it will be found at compile time (if someone calls a wild class with reflection, it will not be found); If you don’t, you might introduce some higher-version libraries, causing unpredictable problems.

  2. Even if it is well documented and compatible, any upgrade needs to be fully verified for stability.

All right, I’m going to get back to fixing bugs.

The resources

  • Google developers – Flutter 1.17 | 2020 released the first stable version!
  • AndroidX version & Core version
  • Csdn-nestedscrolling for full resolution takes you to play with NestedScrolling
  • Android nested sliding and NestedScrollView
  • Simple book – Android source code analysis – nested sliding mechanism implementation principle
  • Gold digger – A real life experience of a common pit when using nested slides