SnapHelper硬核講解

前言

這都9012年了蝌数,SnapHelper不是新鮮玩意打毛,為啥我要拿出來解析物遇?首先,Google已經(jīng)放出 Viewpager2 測試版本娘赴,該方案計劃用RecyclerView替換掉ViewPager规哲;其次,我發(fā)現(xiàn)身邊很多Android同學(xué)SnapHelper了解并不深诽表;所以唉锌,弄懂并熟練使用SnapHelper是必要的;我借著閱讀androidxViewpager2源碼的機會竿奏,跟大家仔細(xì)梳理一下SnapHelper的原理袄简;

SnapHelper認(rèn)識

我忽然覺得有必要科普一下SnapHelper的基本情況,首先SnapHelper是附加于RecyclerView上面的一個輔助功能泛啸,它能讓RecyclerView實現(xiàn)類似ViewPager等功能绿语;如果沒有SnapHelperRecyclerView也能很好的使用候址;但一個普通的RecyclerView在滾動方面和ListView沒有特殊的區(qū)別吕粹,都是給人一種直來直往的感覺,比如我想實現(xiàn)橫向滾動左邊的子View始終左對齊岗仑,或者我用力一滑匹耕,慣性滾動最大距離不能超過一屏,這些看似不屬于RecyclerView的功能赔蒲,有了SnapHelper就很好的解決泌神;所以SnapHelper有它存在的價值良漱,它不是RecyclerView核心功能的參與者,但有它就能錦上添花欢际;

image

RecyclerView滾動基礎(chǔ)

在正式介紹SnapHelper之前母市,先了解一下滾動相關(guān)的基礎(chǔ)知識點,我把RecyclerView的滾動分為滾動狀態(tài)Fling這兩類损趋,主要應(yīng)對的是OnScrollListenerOnFlingListener這兩個回調(diào)接口患久;

滾動狀態(tài)監(jiān)聽

RecyclerVier一共有三種描述滾動的狀態(tài):SCROLL_STATE_IDLESCROLL_STATE_DRAGGING浑槽、SCROLL_STATE_SETTLING蒋失,稍微注釋一下:

  • SCROLL_STATE_IDLE
    • 滾動閑置狀態(tài),此時并沒有手指滑動或者動畫執(zhí)行
  • SCROLL_STATE_DRAGGING
    • 滾動拖拽狀態(tài)桐玻,由于用戶觸摸屏幕產(chǎn)生
  • SCROLL_STATE_SETTLING
    • 自動滾動狀態(tài)篙挽,此時沒有手指觸摸,一般是由動畫執(zhí)行滾動到最終位置镊靴,包括smoothScrollTo等方法的調(diào)用

我們想監(jiān)聽狀態(tài)的改變铣卡,調(diào)用addOnScrollListener方法,重寫OnScrollListener的回調(diào)方法即可偏竟,注意OnScrollListener提供的回調(diào)數(shù)據(jù)并不如ViewPager那樣詳細(xì)煮落,甚至是一種缺陷,這在ViewPager2ScrollEventAdapter類有詳細(xì)的適配方法踊谋,有興趣的可以看看蝉仇。

addOnScrollListener方法是接下來分析SnapHelper的重點之一;

fling行為監(jiān)聽

承接上文殖蚕,自然滾動行為底層的要點是處理fling行為轿衔,flingAndroid View中慣性滾動的代言詞,分析代碼如下:

RecyclerView

public boolean fling(int velocityX, int velocityY) {
    if (mLayout == null) {
        Log.e(TAG, "Cannot fling without a LayoutManager set. " +
                "Call setLayoutManager with a non-null argument.");
        return false;
    }
    if (mLayoutFrozen) {
        return false;
    }
    final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
    final boolean canScrollVertical = mLayout.canScrollVertically();
    if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
        velocityX = 0;
    }
    if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
        velocityY = 0;
    }
    if (velocityX == 0 && velocityY == 0) {
        // If we don't have any velocity, return false
        return false;
    }
    //處理嵌套滾動PreFling
    if (!dispatchNestedPreFling(velocityX, velocityY)) {
        final boolean canScroll = canScrollHorizontal || canScrollVertical;
        //處理嵌套滾動Fling
        dispatchNestedFling(velocityX, velocityY, canScroll);
        //優(yōu)先判斷mOnFlingListener的邏輯
        if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
            return true;
        }

        if (canScroll) {
            velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
            velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
            //默認(rèn)的Fling操作
            mViewFlinger.fling(velocityX, velocityY);
            return true;
        }
    }
    return false;
}

RecyclerViewfling行為流程圖如下:

image

其中mOnFlingListener是通過setOnFlingListener方法設(shè)置嫌褪,這個方法也是接下來分析SnapHelper的重點之一呀枢;

SnapHelper小覷

SnapHelper顧名思義是Snap+Helper的組合胚股,Snap有移到某位置的含義笼痛,Helper譯為輔助者,綜合場景解釋是將RecyclerView移動到某位置的輔助類琅拌,這句話看似簡單明了缨伊,卻蘊藏疑問,有兩個疑問點需要我們弄明白:

何時何地觸發(fā)RecyclerView移動进宝?又要把RecyclerView移到哪個位置刻坊?

帶著這兩個疑問,我們從SnapHelper的使用和入口方法看起:

attachToRecyclerView入口

PagerSnapHelper為例党晋,SnapHelper的基本使用:

 new PagerSnapHelper().attachToRecyclerView(mRecyclerView);

PagerSnapHelperSnapHelper的子類谭胚,徐块,SnapHelper的使用很簡單,只需要調(diào)用attachToRecyclerView綁定到置頂RecyclerView即可灾而;

SnapHelper

public abstract class SnapHelper extends RecyclerView.OnFlingListener 
    //綁定RecyclerView
    public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();//解除歷史回調(diào)的關(guān)系
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            setupCallbacks();//注冊回調(diào)
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
            snapToTargetExistingView();//移動到制定View
        }
    }
    //設(shè)置回調(diào)關(guān)系
    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }

    //注銷回調(diào)關(guān)系
    private void destroyCallbacks() {
        mRecyclerView.removeOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(null);
    }
    
}

SnapHelper是一個抽象類胡控,實現(xiàn)了RecyclerView.OnFlingListener接口,入口方法attachToRecyclerViewSnapHelper中定義旁趟,該方法主要起到清理昼激、綁定回調(diào)關(guān)系和初始化位置的作用,在setupCallbacks中設(shè)置了addOnScrollListenersetOnFlingListener兩種回調(diào)锡搜;

上文說過RecyclerView的滾動狀態(tài)和fling行為的監(jiān)聽橙困,在這里看到SnapHelper對于這兩種行為都需要監(jiān)聽,attachToRecyclerView的主要邏輯就是干這個事的耕餐,至于如何處理回調(diào)之后的事情凡傅,且繼續(xù)往下看;

SnapHelper處理回調(diào)流程

SnapHelperattachToRecyclerView方法中注冊了滾動狀態(tài)和fling的監(jiān)聽肠缔,當(dāng)監(jiān)聽觸發(fā)時像捶,如何處理后續(xù)的流程,我們先分析滾動狀態(tài)的回調(diào):

滾動狀態(tài)回調(diào)處理

滾動狀態(tài)的回調(diào)接口實例是mScrollListener

SnapHelper

private final RecyclerView.OnScrollListener mScrollListener =
         new RecyclerView.OnScrollListener() {
             boolean mScrolled = false;

             @Override
             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                 super.onScrollStateChanged(recyclerView, newState);
                 //靜止?fàn)顟B(tài)且滾動過一段距離桩砰,觸發(fā)snapToTargetExistingView();
                 if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                     mScrolled = false;
                     //移動到指定的已存在的View
                     snapToTargetExistingView();
                 }
             }

             @Override
             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                 if (dx != 0 || dy != 0) {
                     mScrolled = true;
                 }
             }
         };

邏輯處理的入口在onScrollStateChanged方法中拓春,當(dāng)newState == RecyclerView.SCROLL_STATE_IDLE且滾動距離不等于0,觸發(fā)snapToTargetExistingView方法亚隅;

SnapHelper

//移動到指定的已存在的View
void snapToTargetExistingView() {
    if (mRecyclerView == null) {
        return;
    }
    RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
        return;
    }
    //查找SnapView
    View snapView = findSnapView(layoutManager);
    if (snapView == null) {
        return;
    }
    //計算SnapView的距離
    int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
    if (snapDistance[0] != 0 || snapDistance[1] != 0) {
        //調(diào)用smoothScrollBy移動到制定位置
        mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
    }
}

snapToTargetExistingView方法顧名思義是移動到指定已存在的View的位置硼莽,findSnapView是查到目標(biāo)的SnapViewcalculateDistanceToFinalSnap是計算SnapView到最終位置的距離煮纵;由于findSnapViewcalculateDistanceToFinalSnap是抽象方法懂鸵,所以需要子類的具體實現(xiàn);
整理一下滾動狀態(tài)回調(diào)下行疏,SnapHelper的實現(xiàn)流程圖如下匆光;

image

Fling結(jié)果回調(diào)處理

上文分析SnapHelper實現(xiàn)了RecyclerView.OnFlingListener接口,因此Fling的結(jié)果在onFling()方法中實現(xiàn):

@Override
public boolean onFling(int velocityX, int velocityY) {
    RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
        return false;
    }
    RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
    if (adapter == null) {
        return false;
    }
    int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
    return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
            && snapFromFling(layoutManager, velocityX, velocityY);
}
//處理snap的fling邏輯
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
        int velocityY) {
        //判斷l(xiāng)ayoutManager要實現(xiàn)ScrollVectorProvider
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        return false;
    }
    //創(chuàng)建SmoothScroller
    RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
    if (smoothScroller == null) {
        return false;
    }
    //獲得snap position
    int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
    if (targetPosition == RecyclerView.NO_POSITION) {
        return false;
    }
    //設(shè)置position
    smoothScroller.setTargetPosition(targetPosition);
    //啟動SmoothScroll
    layoutManager.startSmoothScroll(smoothScroller);
    //返回true攔截掉后續(xù)的fling操作
    return true;
}

//創(chuàng)建Scroller
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        return null;
    }
    return new LinearSmoothScroller(mRecyclerView.getContext()) {
        @Override
        protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
            if (mRecyclerView == null) {
                // The associated RecyclerView has been removed so there is no action to take.
                return;
            }
            //計算Snap到目標(biāo)位置的距離
            int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                    targetView);
            final int dx = snapDistances[0];
            final int dy = snapDistances[1];
            //計算時間
            final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
            if (time > 0) {
                action.update(dx, dy, time, mDecelerateInterpolator);
            }
        }
        //計算速度
        @Override
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
            return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
        }
    };
}

fling流程分析

  • fling的邏輯主要在snapFromFling方法中酿联,完成fling邏輯首先要求layoutManagerScrollVectorProvider的實現(xiàn)终息,為什么要求實現(xiàn)ScrollVectorProvider?,因為SnapHelper需要知道布局的方向贞让,而ScrollVectorProvider正是該功能的提供者周崭;

  • 其次是創(chuàng)建SmoothScroller,主要邏輯是createSnapScroller方法喳张,該方法有默認(rèn)的實現(xiàn)续镇,主要邏輯是創(chuàng)建一個LinearSmoothScroller,在onTargetFound中調(diào)用calculateDistanceToFinalSnap計算距離销部,然后通過calculateTimeForDeceleration計算動畫時間摸航;

  • 然后通過findTargetSnapPosition方法獲取目標(biāo)targetPosition制跟,最后把targetPosition賦值給smoothScroller,通過layoutManager執(zhí)行該scroller;

  • 最重要的是snapFromFling要返回true酱虎,前文分析過RecyclerView的fling流程凫岖,返回true的話,默認(rèn)的ViewFlinger就不會執(zhí)行逢净。

fling邏輯流程圖如下

image

段落小結(jié)

SnapHelper對于滾動狀態(tài)和Fling行為的處理上面已經(jīng)梳理完畢哥放,我特意畫了兩個草圖,希望讓大家有更清晰的認(rèn)識爹土,如果還不清晰至少得知道怎么用吧甥雕,例如我們要自定義SnapHelper,必須要重寫的三個方法是:

  • findSnapView(RecyclerView.LayoutManager layoutManager)
    • 在滾動狀態(tài)回調(diào)時調(diào)用胀茵,目的是查找SnapView社露,注意返回的SnapView必須是LayoutManager已經(jīng)加載出來的View;
  • calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
    • 計算sanpView到指定位置的距離琼娘,這是在滾動狀態(tài)回調(diào)和Fling的計算時間工程中使用峭弟;
  • findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)
    • 查找指定的SnapPosition,這個方法只有在Fling的時候調(diào)用脱拼;

記住這三個方法瞒瘸,如果想玩轉(zhuǎn)SnapHelper,掌握這個三分方法是邁出的第一步熄浓;

SnapHelper到底怎么玩

往往知道方法怎么用情臭,卻不知道代碼怎么寫,這是最困惑的赌蔑,我們以LinearSnapHelper為例俯在,從細(xì)節(jié)出發(fā),分析自定義SnapHelper的常用思路和關(guān)鍵方法娃惯;

動代碼前跷乐,先弄清這倆哥們到底解決了啥問題,首先LinearSnapHelper能夠讓線性排列的列表元素趾浅,最中間那顆元素居中顯示愕提;下圖是LinearSnapHelper的效果展示之一;

image

findSnapView怎么玩

前面交待過潮孽,findSnapView方法是查找SnapView的揪荣,何為SnapView,在LinearSnapHelper的應(yīng)用場景中往史,屏幕(RecyclerView)中間的View就是SnapView,且看findSnapView方法的實現(xiàn):

LinearSnapHelper

public View findSnapView(RecyclerView.LayoutManager layoutManager) {
    //橫向
    if (layoutManager.canScrollVertically()) {
        return findCenterView(layoutManager, getVerticalHelper(layoutManager));
    } else if (layoutManager.canScrollHorizontally()) {//縱向
        return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
    }
    return null;
}

@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
    if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
        mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
    }
    return mVerticalHelper;
}

@NonNull
private OrientationHelper getHorizontalHelper(
        @NonNull RecyclerView.LayoutManager layoutManager) {
    if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
        mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
    }
    return mHorizontalHelper;
}

首先佛舱,findSnapView中需要判斷RecyclerView滾動的方向椎例,然后拿到對應(yīng)的OrientationHelper,最后通過findCenterView查找到SnapView并返回挨决;

LinearSnapHelper

private View findCenterView(RecyclerView.LayoutManager layoutManager,
        OrientationHelper helper) {
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    }
    View closestChild = null;
    final int center;//中間位置
    //判斷ClipToPadding邏輯
    if (layoutManager.getClipToPadding()) {
        center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    } else {
        center = helper.getEnd() / 2;
    }
    int absClosest = Integer.MAX_VALUE;

    for (int i = 0; i < childCount; i++) {
        final View child = layoutManager.getChildAt(i);
        //child的中間位置
        int childCenter = helper.getDecoratedStart(child) +
                (helper.getDecoratedMeasurement(child) / 2);
        //每個child距離中心位置的差值
        int absDistance = Math.abs(childCenter - center);
        //取距離最小的那個
        if (absDistance < absClosest) {
            absClosest = absDistance;
            closestChild = child;
        }
    }
    return closestChild;
}

findCenterView()方法是獲取屏幕(RecyclerView控件)中間位置最近的那個View當(dāng)做SnapView,計算的過程稍顯復(fù)雜其實比較了然订歪,具體注釋在代碼中標(biāo)注脖祈,容易產(chǎn)生疑惑的是OrientationHelper下面一堆獲取位置的方法,這里稍微總結(jié)一下:

OrientationHelper常見方法

  • getStartAfterPadding() 獲取RecyclerView起始位置刷晋,如果padding不為0盖高,則算上padding;
  • getTotalSpace() 獲取RecyclerView可使用控件,本質(zhì)上是RecyclerView的尺寸減輕兩邊的padding;
  • getDecoratedStart(View) 獲取View的起始位置眼虱,如果RecyclerView有padding喻奥,則算上padding;
  • getDecoratedMeasurement(View) 獲取View寬度捏悬,如果該view有maring撞蚕,也會算上;

總的來說findCenterView并不復(fù)雜过牙,最迷惑人的是OrientationHelper的一堆API甥厦,在使用時稍加注意,也不是很復(fù)雜的寇钉;

calculateDistanceToFinalSnap怎么玩

首先刀疙,calculateDistanceToFinalSnap接受上一步獲取的SnapView,需要返回一個int[]扫倡,該數(shù)組約定長度為2庙洼,第0位表示水平方向的距離,第1位表示豎直方向的距離镊辕,且看LinearSnapHelper怎么玩油够;

LinearSnapHelper

public int[] calculateDistanceToFinalSnap(
        @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
    int[] out = new int[2];
    if (layoutManager.canScrollHorizontally()) {//水平
        out[0] = distanceToCenter(layoutManager, targetView,
                getHorizontalHelper(layoutManager));
    } else {
        out[0] = 0;
    }
    if (layoutManager.canScrollVertically()) {//豎直
        out[1] = distanceToCenter(layoutManager, targetView,
                getVerticalHelper(layoutManager));
    } else {
        out[1] = 0;
    }
    return out;
}
//距離中間位置的距離
private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
        @NonNull View targetView, OrientationHelper helper) {
    //targetView的中心位置(距離RecyclerView start為準(zhǔn))
    final int childCenter = helper.getDecoratedStart(targetView) +
            (helper.getDecoratedMeasurement(targetView) / 2);
    final int containerCenter;  //RecyclerView的中心位置
    if (layoutManager.getClipToPadding()) {
        containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    } else {
        containerCenter = helper.getEnd() / 2;
    }
    return childCenter - containerCenter;//差距
}

很幸運,calculateDistanceToFinalSnap并沒有很復(fù)雜的代碼征懈,主要是計算方向石咬,然后通過OrientationHelper計算第一步findSnapView得到的SnapView距離中間位置的距離;代碼和第一步很相似卖哎,注釋在代碼中鬼悠;

findTargetSnapPosition怎么玩

前面說過,findTargetSnapPosition是處理Fling流程中亏娜,計算SnapPosition的關(guān)鍵方法焕窝,首先,findTargetSnapPosition接受速度參數(shù)velocityXvelocityY维贺,需要返回int類型的position它掂,這個位置對應(yīng)的是Adapter中的position,并不是LayoutManagerRecyclerView中子View的index

LinearSnapHelper

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
        int velocityY) {
        //判斷是否實現(xiàn)ScrollVectorProvider
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        return RecyclerView.NO_POSITION;
    }
    //獲取Adapter中item個數(shù)
    final int itemCount = layoutManager.getItemCount();
    if (itemCount == 0) {
        return RecyclerView.NO_POSITION;
    }
    //查找中間SnapView
    final View currentView = findSnapView(layoutManager);
    if (currentView == null) {
        return RecyclerView.NO_POSITION;
    }
    //計算當(dāng)前View在adapter中的position
    final int currentPosition = layoutManager.getPosition(currentView);
    if (currentPosition == RecyclerView.NO_POSITION) {
        return RecyclerView.NO_POSITION;
    }
    //獲取布局方向提供者
    RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
            (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
    //從當(dāng)前位置往最后一個元素計算
    PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
    if (vectorForEnd == null) {
        return RecyclerView.NO_POSITION;
    }

    int vDeltaJump, hDeltaJump;//計算慣性能滾動多少個子View
    if (layoutManager.canScrollHorizontally()) {//水平
        hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                getHorizontalHelper(layoutManager), velocityX, 0);
        if (vectorForEnd.x < 0) {//豎直為負(fù)表示滾動為負(fù)方向
            hDeltaJump = -hDeltaJump;
        }
    } else {
        hDeltaJump = 0;
    }
    if (layoutManager.canScrollVertically()) {//豎直方向
        vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                getVerticalHelper(layoutManager), 0, velocityY);
        if (vectorForEnd.y < 0) {//豎直為負(fù)表示滾動為負(fù)方向
            vDeltaJump = -vDeltaJump;
        }
    } else {
        vDeltaJump = 0;
    }
    //計算水平和豎直方向
    int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
    if (deltaJump == 0) {
        return RecyclerView.NO_POSITION;
    }
    //計算目標(biāo)position
    int targetPos = currentPosition + deltaJump;
    if (targetPos < 0) {//邊界判斷
        targetPos = 0;
    }
    if (targetPos >= itemCount) {//邊界判斷
        targetPos = itemCount - 1;
    }
    return targetPos;
}

計算通過慣性能滾動多少個子View的代碼:

LinearSnapHelper

private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
        OrientationHelper helper, int velocityX, int velocityY) {
    //慣性能滾動多少距離
    int[] distances = calculateScrollDistance(velocityX, velocityY);
    //單個child平均占用多少寬/高像素
    float distancePerChild = computeDistancePerChild(layoutManager, helper);
    if (distancePerChild <= 0) {
        return 0;
    }
    //得到最終的水平/豎直的距離
    int distance =
            Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
    if (distance > 0) {四舍五入得到平均個數(shù)
        return (int) Math.floor(distance / distancePerChild);
    } else {//負(fù)數(shù)的除法特殊處理得到平均個數(shù)
        return (int) Math.ceil(distance / distancePerChild);
    }
}

計算每個child的平均占用多少寬/高的代碼如下:

LinearSnapHelper

private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
        OrientationHelper helper) {
    View minPosView = null;
    View maxPosView = null;
    int minPos = Integer.MAX_VALUE;
    int maxPos = Integer.MIN_VALUE;
    int childCount = layoutManager.getChildCount();//獲取已經(jīng)加載的View個數(shù)虐秋,不是所有adapter中的count
    if (childCount == 0) {
        return INVALID_DISTANCE;
    }
    //計算已加載View中榕茧,最start和最end的View和Position
    for (int i = 0; i < childCount; i++) {
        View child = layoutManager.getChildAt(i);
        final int pos = layoutManager.getPosition(child);
        if (pos == RecyclerView.NO_POSITION) {
            continue;
        }
        if (pos < minPos) {
            minPos = pos;
            minPosView = child;
        }
        if (pos > maxPos) {
            maxPos = pos;
            maxPosView = child;
        }
    }
    if (minPosView == null || maxPosView == null) {
        return INVALID_DISTANCE;
    }
    //分別獲取最start和最end位置,距RecyclerView起點的距離客给;
    int start = Math.min(helper.getDecoratedStart(minPosView),
            helper.getDecoratedStart(maxPosView));
    int end = Math.max(helper.getDecoratedEnd(minPosView),
            helper.getDecoratedEnd(maxPosView));
    //得到距離的絕對差值
    int distance = end - start;
    if (distance == 0) {
        return INVALID_DISTANCE;
    }
    //計算平均寬/高
    return 1f * distance / ((maxPos - minPos) + 1);
}

LinearSnapHelperfindTargetSnapPosition方法著實不簡單用押,但是條理清晰邏輯嚴(yán)謹(jǐn),考慮的比較周全靶剑,上面代碼我做了比較詳細(xì)的注釋蜻拨,相信肯定有同學(xué)不愛看代碼,我也是桩引,所以我用文字重新梳理一下上述代碼邏輯和關(guān)鍵點缎讼;

  • findTargetSnapPosition方法邏輯流程總結(jié):

    • 首先通過findSnapView()活動當(dāng)前的centerView;
    • 通過ScrollVectorProvider是否是reverseLayout,布局方向;
    • 通過estimateNextPositionDiffForFling方法獲取該慣性能產(chǎn)生多少個子child的平移阐污,或者理解成該慣性能讓RecyclerView滾動多遠(yuǎn)個子child的距離休涤;
    • 通過當(dāng)前的centerView下標(biāo),加上慣性產(chǎn)生的平移笛辟,計算出最終要落地的下標(biāo)功氨;
    • 邊界判斷
  • estimateNextPositionDiffForFling方法邏輯流程總結(jié):

    • 通過calculateScrollDistance計算慣性能滾動多遠(yuǎn)距離;
    • 通過computeDistancePerChild計算平均一個child占多大尺寸手幢;
    • 距離除以尺寸捷凄,四舍五入得到個數(shù)并返回;
  • computeDistancePerChild方法邏輯流程總結(jié):

    • 獲取layoutManager已經(jīng)加載的所有子View;
    • 獲取最start和最end的view和下標(biāo)围来;
    • 分別計算最start和最end的View的start和end值跺涤;
    • 計算平均值并返回;

終于是把LinearSnapHelper的核心邏輯講完了监透,縱觀整個類桶错,主要邏輯還是在findTargetSnapPosition這里,趁熱打鐵胀蛮,我必須跟大家分享一下PagerSnapHelper是如何玩轉(zhuǎn)這個方法的院刁;

PagerSnapHelper似乎更簡單

pagerSnapHelper同樣也實現(xiàn)了SnapHelper的三個方法,下面先看findTargetSnapPosition:

PagerSnapHelper

public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
        int velocityY) {
    final int itemCount = layoutManager.getItemCount();//獲取adapter中所有的itemcount
    if (itemCount == 0) {
        return RecyclerView.NO_POSITION;
    }

    View mStartMostChildView = null;//獲取最start的View
    if (layoutManager.canScrollVertically()) {
        mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
    } else if (layoutManager.canScrollHorizontally()) {
        mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
    }

    if (mStartMostChildView == null) {
        return RecyclerView.NO_POSITION;
    }
    //最start的View當(dāng)前centerposition
    final int centerPosition = layoutManager.getPosition(mStartMostChildView);
    if (centerPosition == RecyclerView.NO_POSITION) {
        return RecyclerView.NO_POSITION;
    }

    final boolean forwardDirection;//速度判定
    if (layoutManager.canScrollHorizontally()) {
        forwardDirection = velocityX > 0;
    } else {
        forwardDirection = velocityY > 0;
    }
    boolean reverseLayout = false;//是否是reverseLayout粪狼,布局方向
    if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd != null) {
            reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
        }
    }
    return reverseLayout
            ? (forwardDirection ? centerPosition - 1 : centerPosition)下標(biāo)要買+1 or -1退腥,要么保持不變
            : (forwardDirection ? centerPosition + 1 : centerPosition);
}

眾所周知,ViewPager的翻頁要么是保持不變再榄,要么是下一頁/上一頁狡刘,上面findTargetSnapPosition方法就是主要的實現(xiàn)邏輯,其中判定是否翻頁的條件由forwardDirection來控制困鸥,直接對比速度>0嗅蔬,用戶想輕松滑到下一頁是比較easy的,以至于上面代碼量少到不敢相信;

至于findSnapViewdistanceToCenter方法购城,同樣是獲取屏幕(RecyclerView)中間的View吕座,計算distanceToCenter虐译,跟LinearSnapHelper如出一轍瘪板;

PagerSnapHelper注意事項

PagerSnapHelper設(shè)計之初是就是適用于一屏(RecyclerView范圍內(nèi))顯示單個child的,如果有一屏顯示多個child的需求漆诽,PagerSnapHelper并不適用侮攀;其實在實際開發(fā)中這種需求還是挺多的,當(dāng)然github上早已經(jīng)有大神寫過一個庫厢拭,實現(xiàn)了幾個常用的SnapHelper場景兰英,github傳送門;當(dāng)然這個庫并不能滿足所有的需求供鸠,有機會再跟大家分享更有意義的SnapHelper實戰(zhàn)畦贸;

結(jié)尾:明明是玩了一場接力賽

什么玩意,接力賽楞捂?沒有錯薄坏。SnapHelper在運行過程中,RecyclerView的狀態(tài)可能會經(jīng)歷這樣DRAGGING->SETTLING->IDLE->SETTLING->IDLE甚至更多狀態(tài)寨闹,我稱之為接力賽胶坠,為什么會這個樣子?拿LinearSnapHelper來說繁堡,前期手勢拖拽沈善,肯定是玩DRAGGING狀態(tài),一旦撒手加之慣性椭蹄,會進(jìn)入SETTLING狀態(tài)闻牡,然后fling()方法會計算snapPosition并指示SmoothScrooler滾動到snapPosition位置,滾動完畢會進(jìn)入IDLE狀態(tài)绳矩,注意SmoothScrooler滾動結(jié)束的位置相對于RecyclerView的start位置的罩润,而LinearSnapHelper要求中間對齊,此時必然會觸發(fā)snapToTargetExistingView()方法埋酬,做最后的調(diào)整哨啃,所謂最后的調(diào)整是通過snapToTargetExistingView調(diào)用smoothScrollBy,而結(jié)束條件通常是calculateDistanceToFinalSnap()返回[0,0]写妥,這就是我所說的接力賽拳球;

陷阱: 一旦calculateDistanceToFinalSnap()返回值計算錯誤,有可能造成RecyclerView進(jìn)入smoothScroolBy的魔鬼循環(huán)局面珍特,直到滾動到頭/尾才會結(jié)束祝峻;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子莱找,更是在濱河造成了極大的恐慌酬姆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奥溺,死亡現(xiàn)場離奇詭異辞色,居然都是意外死亡,警方通過查閱死者的電腦和手機浮定,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門相满,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桦卒,你說我怎么就攤上這事立美。” “怎么了方灾?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵建蹄,是天一觀的道長。 經(jīng)常有香客問我裕偿,道長洞慎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任击费,我火速辦了婚禮拢蛋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔫巩。我一直安慰自己谆棱,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布圆仔。 她就那樣靜靜地躺著垃瞧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坪郭。 梳的紋絲不亂的頭發(fā)上个从,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音歪沃,去河邊找鬼嗦锐。 笑死,一個胖子當(dāng)著我的面吹牛沪曙,可吹牛的內(nèi)容都是我干的奕污。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼液走,長吁一口氣:“原來是場噩夢啊……” “哼碳默!你這毒婦竟也來了贾陷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嘱根,失蹤者是張志新(化名)和其女友劉穎髓废,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體该抒,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡慌洪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了柔逼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒋譬。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡割岛,死狀恐怖愉适,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情癣漆,我是刑警寧澤维咸,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站惠爽,受9級特大地震影響癌蓖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜婚肆,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一租副、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧较性,春花似錦用僧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至攀操,卻和暖如春院仿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背速和。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工歹垫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颠放。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓排惨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親慈迈。 傳聞我的和親對象是個殘疾皇子若贮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容