自定義AppBarLayout,讓它Fling起來更流暢

咱們知道,Desgin包中的AppBarLayout配合CollapsingToolbarLayout能夠實現摺疊效果。可是頂部在快速滑動到摺疊狀態時,底部的NestedScrollChild不會由於慣性跟着滑動,整個滑動過程瞬間中止,給人一種很不流暢的感受。爲了能讓咱們的AppBarLayout能Fling更流暢,咱們須要在從新修改源碼,定製一個FlingAppBarLayout,可以實現相似餓了麼首頁效果 java

餓了麼首頁效果

思路

咱們知道AppBarLayout之因此可以有摺疊效果,是由於有一個默認的Behavior,並且AppBarLayout在快速滑動時,佈局也可以快速展開和收縮,所以能夠猜想內部有可能處理了Fling事件。經過源碼,找到對應的Behavior,它繼承自HeaderBehavior,經過onTouchEvent方法,找到了對應對於Fling事件的處理git

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);
                }
複製代碼

進入fling方法,找到了scroller對象,AppBarLayout的快速滑動效果就是經過它來實現的。至於爲何AppBarLayout向上快速滑動到邊界時,忽然中止,沒有慣性滑動,是由於scroller在調用fling方法時設置了minOffset(向上滑動邊界)github

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;
        }
    }
複製代碼

而具體的view的移動,則是經過FlingRunnable來實現。ide

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() {
            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);
                }
            }
        }
    }
複製代碼

經過這個FlingRunnable類,咱們知道AppBarLayout能快速展開和收縮,就是經過它實現的。佈局

具體實現

首先,咱們把design中的AppBarLayout源碼複製到本身的package中,引入報紅的相關文件,具體以下: post

工程目錄
其中ScrollItem,ReflectUtil,ViewPagerUtil爲咱們本身定義的,其餘都是design包拷貝的。 經過前面三塊代碼,咱們知道AppBarLayout的Fling效果是經過scroller實現的,滑動的邊界時經過minOffset和maxOffset來控制的,當滑動的offset超出範圍時,scroller調用computeScrollerOffset就爲false,頂部view就中止移動了。

所以爲了能讓AppBarLayout在向上滑動到minOffset邊界時不中止移動,把這個minOffset保存到FlingRunnable中,在scroller.fling方法中這個更小的offset,這個在滑動到minOffset時,computeScrollerOffset就不會爲false,而且在FlingRunnable中由於有minOffset,咱們能夠在mScroller.computeScrollOffset裏判斷是否滑出邊界,經過差值,繼續滑動底部的可滑動佈局。this

mScroller.fling(
                0, getTopAndBottomOffset(), // curr
                0, Math.round(velocityY), // velocity.
                0, 0, // x
                minOffset-5000, maxOffset); // 設置一個很大的值,在向上滑動時不會由於低於minOffset而中止滑動
複製代碼

在FlingRunnable中新增minOffset字段,run方法中,若是currY<minOffset表示AppBarLayout向上滑動值收縮狀態,能夠滑動底部佈局了,scrollNext(),傳入偏移量minOffset-currYspa

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() {
            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);
                }
            }
        }
    }
複製代碼

在構造FlingRunnable時傳入minOffsetcode

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 {
           ...
        }
    }
複製代碼

接着就是具體scrollNext方法了,具體就是找到底部的NestedScrollingChild(如RecyclerView,NestedScrollView,ViewPager,主要是這三個)。 在FlingRunnable中新增ScrollItem字段用於處理scroll邏輯cdn

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 = new ScrollItem(v);
                }
            }
        @Override
        public void run() {
            if (mLayout != null && mScroller != null) {
                if (mScroller.computeScrollOffset()) {
                    int currY = mScroller.getCurrY();
                    if (currY < 0 && currY < minOffset) {
                        scrollItem.scroll(minOffset - currY); //處理邏輯在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);
                }
            }
        }
}
複製代碼

而在新增的ScrollItem中,咱們來處理對應的scroll操做(NestedScrollView能夠經過scrollTo,而RecyclerView則須要用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);
    }

    /** * 查找須要滑動的scroll對象 * * @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");
                return findCommonScroll(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;
    }

    /** * 中止NestedScrollView滾動 * * @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();
        }
    }

    /** * 中止RecyclerView滾動 * * @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);
        }
    }

}
複製代碼

至於ViewPager,由於getChildAt會有空值問題,這裏是經過adapter獲取fragment而後獲取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;
    }
}
複製代碼

這裏暫時沒作PagerAdapter的處理邏輯,ViewPager找到當前item界面rootView後,須要找到須要繼續慣性滑動到RecyclerView或NestedScrollView,爲方便查找,咱們給fragment佈局中須要滑動的組件添加tag:「fling」,這樣就能夠經過findViewWithTag("fling")找到它。 好了,基本的滑動邏輯處理完了,咱們本身的AppBarLayout能夠慣性fling了。會看ScrollItem代碼,我加了stopScroll的邏輯。那是由於在底部recyclerView或NestedScrollView快速向下滑動至AppBarLayout展開,而這時在AppBarLayout想要快速向上滑動,應爲底部正在滑動,致使二者衝突,不能正常向上滑動,因此AppBarLayout在向上快速滑動時,要中止底部滑動。經過NestedScrollView和RecyclerView的源碼,咱們找到控制滑動邏輯的OverScroller和ViewFlinger,咱們能夠經過反射來中止對應的滑動。

項目地址

FlingAppBarLayout 具體的效果在github上下載,工程還有關於SmartRefreshLayout兼容適配

相關文章
相關標籤/搜索