1.AppBarLayout nesting slide problem

This problem was discovered when I upgraded the support library version from 25.4.0 to 27.1.1 a few days ago. Found that RecyclerView after sliding to the bottom, there will be nearly a second of stagnation, and then to load the next page of data. We know that the pull-up loading implementation scheme basically listens to the sliding state, and then loads the next page when the sliding stops. The code is basically as follows:

@Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { onLoadNextPage(); }}Copy the code

I looked at a couple of page-loaded pages and found that this was a problem everywhere that used AppBarLayout and RecycleView. So I wrote a simple page to test my guess.

The code for the page layout is very generic, like the following.

<? The XML version = "1.0" encoding = "utf-8"? > <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout app:elevation="0dp" android:layout_width="match_parent" android:layout_height="wrap_content"> <View android:background="@color/colorAccent" app:layout_scrollFlags="scroll|enterAlways" android:layout_width="match_parent" android:layout_height="150dp"/> <View android:background="@color/colorPrimary" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="50dp"/> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView app:layout_behavior="@string/appbar_scrolling_view_behavior" android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"/> </android.support.design.widget.CoordinatorLayout>Copy the code

I used version 25.4.0 first, and I quickly swiped to see the normal result:

Zero is the slide stop. Then came version 27.1.1, with the code unchanged.

Well, 2.5 seconds, longer than I felt… So that means that even though it’s stopped sliding, it’s still sliding. Of course this time is not fixed, it all depends on your hand speed. The faster you slide, the longer it takes, which reminds me of inertial sliding. Let’s take a look at 27.1.1 RecyclerView to realize inertia sliding.

Inertial sliding, so first of all you have to let go while sliding. ACTION_UP in the onTouchEvent method:

@Override public boolean onTouchEvent(MotionEvent e) { ... switch (action) { ... case MotionEvent.ACTION_UP: { mVelocityTracker.addMovement(vtev); / / mobile calculation for a second time how many pixels, mMaxFlingVelocity for speed limit (22000) test machine for mVelocityTracker.com puteCurrentVelocity (1000, mMaxFlingVelocity); final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0; final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0; // The fling method determines whether there is an inertial slide. If it is true, the slide state is not SCROLL_STATE_IDLE. if (! ((xvel ! = 0 || yvel ! = 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } resetTouch(); } break; }... return true; }Copy the code

Fling method:

public boolean fling(int velocityX, int velocityY) { ... if (! dispatchNestedPreFling(velocityX, velocityY)) { final boolean canScroll = canScrollHorizontal || canScrollVertical; dispatchNestedFling(velocityX, velocityY, canScroll); if (canScroll) { int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontal) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertical) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); MViewFlinger. Fling (velocityX, velocityY); return true; } } return false; }Copy the code

ViewFlinger has a lot of code, so LET me simplify it:

static final Interpolator sQuinticInterpolator = new Interpolator() { @Override public float getInterpolation(float t) { T - = 1.0 f; Return t * t * t * t * t + 1.0f; }}; class ViewFlinger implements Runnable { private OverScroller mScroller; Interpolator mInterpolator = sQuinticInterpolator; ViewFlinger() { mScroller = new OverScroller(getContext(), sQuinticInterpolator); } @Override public void run() { final OverScroller scroller = mScroller; / / determine whether completed the entire sliding the if (scroller.com puteScrollOffset ()) {if (dispatchNestedPreScroll (dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {} if (! dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null, TYPE_NON_TOUCH){} if (scroll.isfinished ()) {// Inertia slide ends, state set to SCROLL_STATE_IDLE setScrollState(SCROLL_STATE_IDLE); stopNestedScroll(TYPE_NON_TOUCH); }}} // Inertial sliding, SetScrollState (Scroll_state_explained) public void Fling (int velocityX, int velocityY) {setScrollState(scroll_state_explained); mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);  }... }Copy the code

The sQuinticInterpolator is a curve of inertial sliding time versus distance, roughly as follows (first fast then slow) :

The Fling method in OverScroller calculates the distance and time needed to slide from the incoming speed value. The higher the velocity, the higher the value. The maximum speed of my test machine is 22,000, so the maximum time calculated is 2632ms. This also matches the information we printed out at the beginning. Computational methods interested can go to see the source code to find out.

So what’s the problem? I compared the two versions of the ViewFlinger code.

DispatchNestedPreScroll, dispatchNestedScroll, hasNestedScrollingParent, stopNestedScroll are not found in 25.4.0. In fact, the purpose of this section is to solve a sliding out of sync bug. Here’s the picture :(the picture is a little… See: Fix for nested scrolling in AppBarLayout in the Design library.)

RecyclerView does not notify AppBarLayout in the fling process, so after the fling ends, AppBarLayout does not know the current position of RecyclerView, so the slide is interrupted. In fact, the cause of the slide jam problem is all here.

So starting at 26+ fixes this problem, which is the change seen above. But that creates a new problem, the stagnation problem I mentioned at the beginning. The problem is the hasNestedScrollingParent method, which determines whether the parent View supports nested sliding. Obviously, there is always a parent View in the nested slide scenario, so SCROLL_STATE_IDLE will be received only after the slide is complete.

if (scroller.isFinished() || (! fullyConsumedAny && ! true)) {}Copy the code

This is why there are no issues with the 25.4.0 version and no nested slides with AppBarLayout.

2. Solutions

Knowing the cause, how to solve it?

1. Upgrade the version

Upgrade to above 28.0.0, all the above problems are resolved. I took a look at the current 28.0.0-RC02 release and found that there are official changes to address this issue. Let’s compare:

27.1.1

28.0.0 – rc02

You can see that the stopNestedScrollIfNeeded method has been added to stop the view’s scrolling while sliding up to the top and down to the bottom.

2. Ideas for reference

If you are between 26 and 28, you can refer to the official solution

public class FixAppBarLayoutBehavior extends AppBarLayout.Behavior { public FixAppBarLayoutBehavior() { super(); } public FixAppBarLayoutBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); stopNestedScrollIfNeeded(dy, child, target, type); } @Override public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); stopNestedScrollIfNeeded(dyUnconsumed, child, target, type); } private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) { if (type == ViewCompat.TYPE_NON_TOUCH) { final int currOffset = getTopAndBottomOffset(); if ((dy < 0 && currOffset == 0) || (dy > 0 && currOffset == -child.getTotalScrollRange())) { ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH); }}}}Copy the code

Use:

    <android.support.design.widget.AppBarLayout
            ...
            app:layout_behavior="yourPackage.FixAppBarLayoutBehavior">Copy the code

Or:

AppBarLayout mAppBarLayout = findViewById(R.id.app_bar);
((CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams()).setBehavior(new FixAppBarLayoutBehavior());Copy the code

3. The other

  1. If you are under 26, upgrade to above 26 is recommended. After all, the authorities have solved the problem. The NestedScrollingParent2 and NestedScrollingChild2 interfaces have been upgraded for this purpose, and NestedScrollType has been added to distinguish between manually triggered slides and non-manual (inertial) slides.

  2. Why not start with RecyclerView? I thought about the principle and sliding conflict is similar, there are external interception, internal interception. Give the initiative to the parent class, more reasonable, more flexible and convenient processing.

Reference 3.

  • Custom control inertial sliding

  • Android8.0 for CoordinatorLayout, RecyclerView precision fling optimization