SnapHelper硬核講解

前言

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

SnapHelper認(rèn)識

我忽然覺得有必要科普一下SnapHelper的基本情況,首先SnapHelper是附加于RecyclerView上面的一個輔助功能,它能讓RecyclerView實現(xiàn)類似ViewPager等功能;如果沒有SnapHelper,RecyclerView也能很好的使用;但一個普通的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移動到某位置的輔助類,這句話看似簡單明了,卻蘊(yùn)藏疑問,有兩個疑問點需要我們弄明白:

何時何地觸發(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)的SnapView,calculateDistanceToFinalSnap是計算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;//差距
}

很幸運(yùn),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)然這個庫并不能滿足所有的需求,有機(jī)會再跟大家分享更有意義的SnapHelper實戰(zhàn);

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

什么玩意,接力賽?沒有錯。SnapHelper在運(yùn)行過程中,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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容