SnapHelper硬核講解

前言

這都9012年了,SnapHelper不是新鮮玩意,爲啥我要拿出來解析?首先,Google已經放出 Viewpager2 測試版本,該方案計劃用RecyclerView替換掉ViewPager;其次,我發現身邊不少Android同窗SnapHelper瞭解並不深;因此,弄懂並熟練使用SnapHelper是必要的;我藉着閱讀androidxViewpager2源碼的機會,跟你們仔細梳理一下SnapHelper的原理;android

SnapHelper認識

我突然以爲有必要科普一下SnapHelper的基本狀況,首先SnapHelper是附加於RecyclerView上面的一個輔助功能,它能讓RecyclerView實現相似ViewPager等功能;若是沒有SnapHelperRecyclerView也能很好的使用;但一個普通的RecyclerView在滾動方面和ListView沒有特殊的區別,都是給人一種直來直往的感受,好比我想實現橫向滾動左邊的子View始終左對齊,或者我用力一滑,慣性滾動最大距離不能超過一屏,這些看似不屬於RecyclerView的功能,有了SnapHelper就很好的解決;因此SnapHelper有它存在的價值,它不是RecyclerView核心功能的參與者,但有它就能錦上添花; git

RecyclerView滾動基礎

在正式介紹SnapHelper以前,先了解一下滾動相關的基礎知識點,我把RecyclerView的滾動分爲滾動狀態Fling這兩類,主要應對的是OnScrollListenerOnFlingListener這兩個回調接口;github

滾動狀態監聽

RecyclerVier一共有三種描述滾動的狀態:SCROLL_STATE_IDLESCROLL_STATE_DRAGGINGSCROLL_STATE_SETTLING,稍微註釋一下:數組

  • SCROLL_STATE_IDLE
    • 滾動閒置狀態,此時並無手指滑動或者動畫執行
  • SCROLL_STATE_DRAGGING
    • 滾動拖拽狀態,因爲用戶觸摸屏幕產生
  • SCROLL_STATE_SETTLING
    • 自動滾動狀態,此時沒有手指觸摸,通常是由動畫執行滾動到最終位置,包括smoothScrollTo等方法的調用

咱們想監聽狀態的改變,調用addOnScrollListener方法,重寫OnScrollListener的回調方法便可,注意OnScrollListener提供的回調數據並不如ViewPager那樣詳細,甚至是一種缺陷,這在ViewPager2ScrollEventAdapter類有詳細的適配方法,有興趣的能夠看看。bash

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

fling行爲監聽

承接上文,天然滾動行爲底層的要點是處理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); //優先判斷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)); //默認的Fling操做 mViewFlinger.fling(velocityX, velocityY); return true; } } return false; } 複製代碼

RecyclerViewfling行爲流程圖以下:測試

其中mOnFlingListener是經過setOnFlingListener方法設置,這個方法也是接下來分析SnapHelper的重點之一;動畫

SnapHelper小覷

SnapHelper顧名思義是Snap+Helper的組合,Snap有移到某位置的含義,Helper譯爲輔助者,綜合場景解釋是將RecyclerView移動到某位置的輔助類,這句話看似簡單明瞭,卻蘊藏疑問,有兩個疑問點須要咱們弄明白:

什麼時候何地觸發RecyclerView移動?又要把RecyclerView移到哪一個位置?

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

attachToRecyclerView入口

PagerSnapHelper爲例,SnapHelper的基本使用:

new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
複製代碼

PagerSnapHelperSnapHelper的子類,,SnapHelper的使用很簡單,只須要調用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();//解除歷史回調的關係
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            setupCallbacks();//註冊回調
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
            snapToTargetExistingView();//移動到制定View
        }
    }
    //設置回調關係
    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }

    //註銷回調關係
    private void destroyCallbacks() {
        mRecyclerView.removeOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(null);
    }
    
}
複製代碼

SnapHelper是一個抽象類,實現了RecyclerView.OnFlingListener接口,入口方法attachToRecyclerViewSnapHelper中定義,該方法主要起到清理、綁定回調關係和初始化位置的做用,在setupCallbacks中設置了addOnScrollListenersetOnFlingListener兩種回調;

上文說過RecyclerView的滾動狀態和fling行爲的監聽,在這裏看到SnapHelper對於這兩種行爲都須要監聽,attachToRecyclerView的主要邏輯就是幹這個事的,至於如何處理回調以後的事情,且繼續往下看;

SnapHelper處理回調流程

SnapHelperattachToRecyclerView方法中註冊了滾動狀態和fling的監聽,當監聽觸發時,如何處理後續的流程,咱們先分析滾動狀態的回調:

滾動狀態回調處理

滾動狀態的回調接口實例是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);
                 //靜止狀態且滾動過一段距離,觸發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方法中,當newState == RecyclerView.SCROLL_STATE_IDLE且滾動距離不等於0,觸發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) {
        //調用smoothScrollBy移動到制定位置
        mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
    }
}
複製代碼

snapToTargetExistingView方法顧名思義是移動到指定已存在的View的位置,findSnapView是查到目標的SnapViewcalculateDistanceToFinalSnap是計算SnapView到最終位置的距離;因爲findSnapViewcalculateDistanceToFinalSnap是抽象方法,因此須要子類的具體實現; 整理一下滾動狀態回調下,SnapHelper的實現流程圖以下;

Fling結果回調處理

上文分析SnapHelper實現了RecyclerView.OnFlingListener接口,所以Fling的結果在onFling()方法中實現:

@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) {
        //判斷layoutManager要實現ScrollVectorProvider
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        return false;
    }
    //建立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;
    }
    //設置position
    smoothScroller.setTargetPosition(targetPosition);
    //啓動SmoothScroll
    layoutManager.startSmoothScroll(smoothScroller);
    //返回true攔截掉後續的fling操做
    return true;
}

//建立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到目標位置的距離
            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的實現,爲何要求實現ScrollVectorProvider?,由於SnapHelper須要知道佈局的方向,而ScrollVectorProvider正是該功能的提供者;

  • 其次是建立SmoothScroller,主要邏輯是createSnapScroller方法,該方法有默認的實現,主要邏輯是建立一個LinearSmoothScroller,在onTargetFound中調用calculateDistanceToFinalSnap計算距離,而後經過calculateTimeForDeceleration計算動畫時間;

  • 而後經過findTargetSnapPosition方法獲取目標targetPosition,最後把targetPosition賦值給smoothScroller,經過layoutManager執行該scroller;

  • 最重要的是snapFromFling要返回true,前文分析過RecyclerView的fling流程,返回true的話,默認的ViewFlinger就不會執行。

fling邏輯流程圖以下

段落小結

SnapHelper對於滾動狀態和Fling行爲的處理上面已經梳理完畢,我特地畫了兩個草圖,但願讓你們有更清晰的認識,若是還不清晰至少得知道怎麼用吧,例如咱們要自定義SnapHelper,必需要重寫的三個方法是:

  • findSnapView(RecyclerView.LayoutManager layoutManager)
    • 在滾動狀態回調時調用,目的是查找SnapView,注意返回的SnapView必須是LayoutManager已經加載出來的View;
  • calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
    • 計算sanpView到指定位置的距離,這是在滾動狀態回調和Fling的計算時間工程中使用;
  • findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)
    • 查找指定的SnapPosition,這個方法只有在Fling的時候調用;

記住這三個方法,若是想玩轉SnapHelper,掌握這個三分方法是邁出的第一步;

SnapHelper到底怎麼玩

每每知道方法怎麼用,殊不知道代碼怎麼寫,這是最困惑的,咱們以LinearSnapHelper爲例,從細節出發,分析自定義SnapHelper的經常使用思路和關鍵方法;

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

findSnapView怎麼玩

前面交待過,findSnapView方法是查找SnapView的,何爲SnapView,在LinearSnapHelper的應用場景中,屏幕(RecyclerView)中間的View就是SnapView,且看findSnapView方法的實現:

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滾動的方向,而後拿到對應的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當作SnapView,計算的過程稍顯複雜其實比較瞭然,具體註釋在代碼中標註,容易產生疑惑的是OrientationHelper下面一堆獲取位置的方法,這裏稍微總結一下:

OrientationHelper常見方法

  • getStartAfterPadding() 獲取RecyclerView起始位置,若是padding不爲0,則算上padding;
  • getTotalSpace() 獲取RecyclerView可以使用控件,本質上是RecyclerView的尺寸減輕兩邊的padding;
  • getDecoratedStart(View) 獲取View的起始位置,若是RecyclerView有padding,則算上padding;
  • getDecoratedMeasurement(View) 獲取View寬度,若是該view有maring,也會算上;

總的來講findCenterView並不複雜,最迷惑人的是OrientationHelper的一堆API,在使用時稍加註意,也不是很複雜的;

calculateDistanceToFinalSnap怎麼玩

首先,calculateDistanceToFinalSnap接受上一步獲取的SnapView,須要返回一個int[],該數組約定長度爲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爲準)
    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並無很複雜的代碼,主要是計算方向,而後經過OrientationHelper計算第一步findSnapView獲得的SnapView距離中間位置的距離;代碼和第一步很類似,註釋在代碼中;

findTargetSnapPosition怎麼玩

前面說過,findTargetSnapPosition是處理Fling流程中,計算SnapPosition的關鍵方法,首先,findTargetSnapPosition接受速度參數velocityXvelocityY,須要返回int類型的position,這個位置對應的是Adapter中的position,並非LayoutManagerRecyclerView中子View的index

LinearSnapHelper

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
        int velocityY) {
        //判斷是否實現ScrollVectorProvider
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        return RecyclerView.NO_POSITION;
    }
    //獲取Adapter中item個數
    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;
    }
    //計算當前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;
    //從當前位置往最後一個元素計算
    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) {//豎直爲負表示滾動爲負方向
            hDeltaJump = -hDeltaJump;
        }
    } else {
        hDeltaJump = 0;
    }
    if (layoutManager.canScrollVertically()) {//豎直方向
        vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                getVerticalHelper(layoutManager), 0, velocityY);
        if (vectorForEnd.y < 0) {//豎直爲負表示滾動爲負方向
            vDeltaJump = -vDeltaJump;
        }
    } else {
        vDeltaJump = 0;
    }
    //計算水平和豎直方向
    int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
    if (deltaJump == 0) {
        return RecyclerView.NO_POSITION;
    }
    //計算目標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) {四捨五入獲得平均個數
        return (int) Math.floor(distance / distancePerChild);
    } else {//負數的除法特殊處理獲得平均個數
        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();//獲取已經加載的View個數,不是全部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方法着實不簡單,可是條理清晰邏輯嚴謹,考慮的比較周全,上面代碼我作了比較詳細的註釋,相信確定有同窗不愛看代碼,我也是,因此我用文字從新梳理一下上述代碼邏輯和關鍵點;

  • findTargetSnapPosition方法邏輯流程總結:

    • 首先經過findSnapView()活動當前的centerView;
    • 經過ScrollVectorProvider是不是reverseLayout,佈局方向;
    • 經過estimateNextPositionDiffForFling方法獲取該慣性能產生多少個子child的平移,或者理解成該慣性能讓RecyclerView滾動多遠個子child的距離;
    • 經過當前的centerView下標,加上慣性產生的平移,計算出最終要落地的下標;
    • 邊界判斷
  • estimateNextPositionDiffForFling方法邏輯流程總結:

    • 經過calculateScrollDistance計算慣性能滾動多遠距離;
    • 經過computeDistancePerChild計算平均一個child佔多大尺寸;
    • 距離除以尺寸,四捨五入獲得個數並返回;
  • computeDistancePerChild方法邏輯流程總結:

    • 獲取layoutManager已經加載的全部子View;
    • 獲取最start和最end的view和下標;
    • 分別計算最start和最end的View的start和end值;
    • 計算平均值並返回;

終因而把LinearSnapHelper的核心邏輯講完了,縱觀整個類,主要邏輯仍是在findTargetSnapPosition這裏,趁熱打鐵,我必須跟你們分享一下PagerSnapHelper是如何玩轉這個方法的;

PagerSnapHelper彷佛更簡單

pagerSnapHelper一樣也實現了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當前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)下標要買+1 or -1,要麼保持不變
            : (forwardDirection ? centerPosition + 1 : centerPosition);
}
複製代碼

衆所周知,ViewPager的翻頁要麼是保持不變,要麼是下一頁/上一頁,上面findTargetSnapPosition方法就是主要的實現邏輯,其中斷定是否翻頁的條件由forwardDirection來控制,直接對比速度>0,用戶想輕鬆滑到下一頁是比較easy的,以致於上面代碼量少到不敢相信;

至於findSnapViewdistanceToCenter方法,一樣是獲取屏幕(RecyclerView)中間的View,計算distanceToCenter,跟LinearSnapHelper一模一樣;

PagerSnapHelper注意事項

PagerSnapHelper設計之初是就是適用於一屏(RecyclerView範圍內)顯示單個child的,若是有一屏顯示多個child的需求,PagerSnapHelper並不適用;其實在實際開發中這種需求仍是挺多的,固然github上早已經有大神寫過一個庫,實現了幾個經常使用的SnapHelper場景,github傳送門;固然這個庫並不能知足全部的需求,有機會再跟你們分享更有意義的SnapHelper實戰;

結尾:明明是玩了一場接力賽

什麼玩意,接力賽?沒有錯。SnapHelper在運行過程當中,RecyclerView的狀態可能會經歷這樣DRAGGING->SETTLING->IDLE->SETTLING->IDLE甚至更多狀態,我稱之爲接力賽,爲何會這個樣子?拿LinearSnapHelper來講,前期手勢拖拽,確定是玩DRAGGING狀態,一旦撒手加之慣性,會進入SETTLING狀態,而後fling()方法會計算snapPosition並指示SmoothScrooler滾動到snapPosition位置,滾動完畢會進入IDLE狀態,注意SmoothScrooler滾動結束的位置相對於RecyclerView的start位置的,而LinearSnapHelper要求中間對齊,此時必然會觸發snapToTargetExistingView()方法,作最後的調整,所謂最後的調整是經過snapToTargetExistingView調用smoothScrollBy,而結束條件一般是calculateDistanceToFinalSnap()返回[0,0],這就是我所說的接力賽;

陷阱: 一旦calculateDistanceToFinalSnap()返回值計算錯誤,有可能形成RecyclerView進入smoothScroolBy的魔鬼循環局面,直到滾動到頭/尾纔會結束;

相關文章
相關標籤/搜索