As we know, the Desgin package AppBarLayout collapses in collaboration with CollapsingToolbarLayout. However, when the top slides quickly to the folded state, the bottom NestedScrollChild will not follow the slide due to inertia, and the whole sliding process will stop instantly, giving a very awkward feeling. In order to Fling our AppBarLayout more smoothly, we need to modify the source code and customize a FlingAppBarLayout to achieve the same effect as the first page

Train of thought

We know that AppBarLayout folds because it has a default Behavior, and that the layout expands and contracts quickly when AppBarLayout slides quickly, so we can predict that the Fling event is handled inside AppBarLayout. Use the source code to find the Behavior that inherits from HeaderBehavior and the onTouchEvent method to find the processing of the Fling event

    case MotionEvent.ACTION_UP:
                if(mVelocityTracker ! =null) {
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
Copy the code

Go to the Fling method and find the Scroller object that lets AppBarLayout slide quickly. The reason why AppBarLayout suddenly stops when it slides up to the edge is that Scroller sets minOffset when it calls the Fling method.

    final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
            int maxOffset, float velocityY) {
        if(mFlingRunnable ! =null) {
            layout.removeCallbacks(mFlingRunnable);
            mFlingRunnable = null;
        }

        if (mScroller == null) {
            mScroller = new OverScroller(layout.getContext());
        }

        mScroller.fling(
                0, getTopAndBottomOffset(), // curr
                0, Math.round(velocityY), // velocity.
                0.0.// x
                minOffset, maxOffset); // y

        if (mScroller.computeScrollOffset()) {
            mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
            ViewCompat.postOnAnimation(layout, mFlingRunnable);
            return true;
        } else {
            onFlingFinished(coordinatorLayout, layout);
            return false; }}Copy the code

The specific view movement is implemented by FlingRunnable.

 private class FlingRunnable implements Runnable {
        private final CoordinatorLayout mParent;
        private final V mLayout;

        FlingRunnable(CoordinatorLayout parent, V layout) {
            mParent = parent;
            mLayout = layout;
        }

        @Override
        public void run(a) {
            if(mLayout ! =null&& mScroller ! =null) {
                if (mScroller.computeScrollOffset()) {
                    setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
                    // Post ourselves so that we run on the next animation
                    ViewCompat.postOnAnimation(mLayout, this);
                } else{ onFlingFinished(mParent, mLayout); }}}}Copy the code

The FlingRunnable class allows you to quickly expand and shrink AppBarLayout.

The specific implementation

First of all, we copy the source code of AppBarLayout in Design into our package, and introduce the relevant files in red, as follows:

So in order to keep the AppBarLayout moving when it slides up to the minOffset boundary, save the minOffset in FlingRunnable. In the Scroll. fling method, the minOffset is smaller. ComputeScrollerOffset I would not be false, and because there are in FlingRunnable minOffset, we can determine whether in mScroller.com puteScrollOffset slip out of the border, through the difference, Continue sliding on the bottom of the slidable layout.

  mScroller.fling(
                0, getTopAndBottomOffset(), // curr
                0, Math.round(velocityY), // velocity.
                0.0.// x
                minOffset-5000, maxOffset); // Set a large value so that sliding up does not stop below minOffset
Copy the code

New minOffset field in FlingRunnable. In the run method, if currY<minOffset indicates the shrinking state of AppBarLayout, you can slide the bottom layout, scrollNext(), and pass the offset minoffset-curry

 class FlingRunnable implements Runnable {
        private final CoordinatorLayout mParent;
        private final V mLayout;
        private int minOffset;

        FlingRunnable(CoordinatorLayout parent, V layout, int min) {
            mParent = parent;
            mLayout = layout;
            minOffset = min;
        }
        @Override
        public void run(a) {
            if(mLayout ! =null&& mScroller ! =null) {
                if (mScroller.computeScrollOffset()) {
                    int currY = mScroller.getCurrY();
                    if (currY < 0 && currY < minOffset) {
                        scrollNext(minOffset - currY);
                        setHeaderTopBottomOffset(mParent, mLayout, minOffset);
                    } else {
                        setHeaderTopBottomOffset(mParent, mLayout, currY);
                    }
                    // Post ourselves so that we run on the next animation
                    ViewCompat.postOnAnimation(mLayout, this);
                } else{ onFlingFinished(mParent, mLayout); }}}}Copy the code

Pass in the minOffset when constructing FlingRunnable

final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
                        int maxOffset, float velocityY) {
        if(mFlingRunnable ! =null) {
            layout.removeCallbacks(mFlingRunnable);
            mFlingRunnable = null;
        }
        if (mScroller == null) {
            mScroller = new OverScroller(layout.getContext());
        }
        mScroller.fling(
                0, getTopAndBottomOffset(), // curr
                0, Math.round(velocityY), // velocity.
                0.0.// x
                minOffset-5000, maxOffset); // y

        if (mScroller.computeScrollOffset()) {
            mFlingRunnable = new FlingRunnable(coordinatorLayout, layout, minOffset);
            ViewCompat.postOnAnimation(layout, mFlingRunnable);
            return true;
        } else{... }}Copy the code

Then there is the specific scrollNext method, concrete is found at the bottom of the NestedScrollingChild (such as RecyclerView NestedScrollView ViewPager, mainly the three). FlingRunnable added a ScrollItem field to handle scroll logic


    class FlingRunnable implements Runnable {
        private final CoordinatorLayout mParent;
        private final V mLayout;
        private int minOffset;
        private ScrollItem scrollItem;


        FlingRunnable(CoordinatorLayout parent, V layout, int min) {
            mParent = parent;
            mLayout = layout;
            minOffset = min;
            initNextScrollView(parent);
        }

        private void initNextScrollView(CoordinatorLayout parent) {
            int count = parent.getChildCount();
            for (int i = 0; i < count; i++) {
                View v = parent.getChildAt(i);
                CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) v.getLayoutParams();
                if (lp.getBehavior() instanceof AppBarLayout.ScrollingViewBehavior) {
                    scrollItem = newScrollItem(v); }}@Override
        public void run(a) {
            if(mLayout ! =null&& mScroller ! =null) {
                if (mScroller.computeScrollOffset()) {
                    int currY = mScroller.getCurrY();
                    if (currY < 0 && currY < minOffset) {
                        scrollItem.scroll(minOffset - currY); // The processing logic is in ScrollItem
                        setHeaderTopBottomOffset(mParent, mLayout, minOffset);
                    } else {
                        setHeaderTopBottomOffset(mParent, mLayout, currY);
                    }
                    // Post ourselves so that we run on the next animation
                    ViewCompat.postOnAnimation(mLayout, this);
                } else{ onFlingFinished(mParent, mLayout); }}}}Copy the code

In the new ScrollItem, we will handle the corresponding scroll operation (NestedScrollView can be scrollTo, and RecyclerView needs to be LinearLayoutManager).

public class ScrollItem {
    private int type; //1: NestedScrollView 2:RecyclerView
    private WeakReference<NestedScrollView> scrollViewRef;
    private WeakReference<LinearLayoutManager> layoutManagerRef;

    public ScrollItem(View v) {
        findScrollItem(v);
    }

    /** * find the scroll object ** to slide@param v
     */
    protected boolean findScrollItem(View v) {
        if (findCommonScroll(v)) return true;
        if (v instanceof ViewPager) {
            View root = ViewPagerUtil.findCurrent((ViewPager) v);
            if(root ! =null) {
                View child = root.findViewWithTag("fling");
                returnfindCommonScroll(child); }}return false;
    }

    private boolean findCommonScroll(View v) {
        if (v instanceof NestedScrollView) {
            type = 1;
            scrollViewRef = new WeakReference<NestedScrollView>((NestedScrollView) v);
            stopScroll(scrollViewRef.get());
            return true;
        }
        if (v instanceof RecyclerView) {
            RecyclerView.LayoutManager lm = ((RecyclerView) v).getLayoutManager();
            if (lm instanceof LinearLayoutManager) {
                LinearLayoutManager llm = (LinearLayoutManager) lm;
                type = 2;
                layoutManagerRef = new WeakReference<LinearLayoutManager>(llm);
                stopScroll((RecyclerView) v);
                return true; }}return false;
    }

    /** * Stop NestedScrollView from scrolling **@param v
     */
    private void stopScroll(NestedScrollView v) {
        try {
            Field field = ReflectUtil.getDeclaredField(v, "mScroller");
            if (field == null) return;
            field.setAccessible(true);
            OverScroller scroller = (OverScroller) field.get(v);
            if(scroller ! =null) scroller.abortAnimation();
        } catch(Exception e) { e.printStackTrace(); }}/** * Stop RecyclerView scrolling **@param* /
    private void stopScroll(RecyclerView rv) {
        try {
            Field field = ReflectUtil.getDeclaredField(rv, "mViewFlinger");
            if (field == null) return;
            field.setAccessible(true);
            Object obj = field.get(rv);
            if (obj == null) return;
            Method method = obj.getClass().getDeclaredMethod("stop");
            method.setAccessible(true);
            method.invoke(obj);
        } catch(Exception e) { e.printStackTrace(); }}public void scroll(int dy) {
        if (type == 1) {
            scrollViewRef.get().scrollTo(0, dy);
        } else if (type == 2) {
            layoutManagerRef.get().scrollToPositionWithOffset(0, -dy); }}}Copy the code

In the case of ViewPager, since getChildAt will have empty values, here we get the fragment through adapter and then get the rootView

public class ViewPagerUtil {
    public static View findCurrent(ViewPager vp) {
        int position = vp.getCurrentItem();
        PagerAdapter adapter = vp.getAdapter();
        if (adapter instanceof FragmentStatePagerAdapter) {
            FragmentStatePagerAdapter fsp = (FragmentStatePagerAdapter) adapter;
            return fsp.getItem(position).getView();
        } else if (adapter instanceof FragmentPagerAdapter) {
            FragmentPagerAdapter fp = (FragmentPagerAdapter) adapter;
            return fp.getItem(position).getView();
        }
        return null; }}Copy the code

PagerAdapter processing logic is not done here. After ViewPager finds the current item interface rootView, it needs to continue to slide inertia to RecyclerView or NestedScrollView. We attach a tag to the component in the Fragment layout: “Fling” and find it by findViewWithTag(” Fling “). Now that the basic sliding logic is done, our AppBarLayout is inertial. Can look at ScrollItem code, I added stopScroll logic. That is because at the bottom of recyclerView or NestedScrollView quickly slide down to AppBarLayout to expand, while at this time, AppBarLayout wants to slide up quickly, because the bottom is sliding, resulting in a conflict between the two, can not normally slide up. So AppBarLayout stops the bottom slide when it zips up. Through the source code of NestedScrollView and RecyclerView, we find the OverScroller and ViewFlinger that control the sliding logic, and we can stop the corresponding sliding by reflection.

The project address

FlingAppBarLayout details can be downloaded on Github, and the project is compatible with SmartRefreshLayout