SnapHelper源碼深度解析

目錄介紹

  • 01.SnapHelper簡單介紹php

    • 1.1 SnapHelper做用
    • 1.2 SnapHelper類分析
    • 1.3 LinearSnapHelper類分析
    • 1.4 PagerSnapHelper類分析
  • 02.SnapHelper源碼分析git

    • 2.1 attachToRecyclerView入口方法
    • 2.2 SnapHelper的抽象方法
    • 2.3 onFling方法源碼分析
  • 03.LinearSnapHelper源碼分析github

    • 3.1 LinearSnapHelper實現功能
    • 3.2 calculateDistanceToFinalSnap()方法源碼
    • 3.3 findSnapView()方法源碼
    • 3.4 findTargetSnapPosition()方法源碼
    • 3.5 支持哪些LayoutManager
    • 3.6 OrientationHelper類
    • 3.7 estimateNextPositionDiffForFling計算偏移量
  • 04.自定義SnapHelper類面試

    • 4.1 業務需求
    • 4.2 自定義helper類

好消息

  • 博客筆記大彙總【16年3月到至今】,包括Java基礎及深刻知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,固然也在工做之餘收集了大量的面試題,長期更新維護而且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計47篇[近20萬字],轉載請註明出處,謝謝!
  • 連接地址:https://github.com/yangchong2...
  • 若是以爲好,能夠star一下,謝謝!固然也歡迎提出建議,萬事起於忽微,量變引發質變!

01.SnapHelper簡單介紹

1.1 SnapHelper做用

  • 在某些場景下,卡片列表滑動瀏覽[有的叫輪播圖],但願當滑動中止時能夠將當前卡片停留在屏幕某個位置,好比停在左邊,以吸引用戶的焦點。那麼可使用RecyclerView + Snaphelper來實現,SnapHelper旨在支持RecyclerView的對齊方式,也就是經過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何像素點。

1.2 SnapHelper類分析

  • 查閱可知,SnapHelper繼承自RecyclerView.OnFlingListener,而且重寫了onFling方法,這個類代碼並很少,下面會對重要方法一一解析。segmentfault

    • 支持SnapHelper的RecyclerView.LayoutManager必須實現的方式:數組

      • RecyclerView.SmoothScroller.ScrollVectorProvider接口
      • 或者本身實現onFling(int,int)方法手動處理邏輯。
  • SnapHelper類重要的方法markdown

    • attachToRecyclerView: 將SnapHelper attach 到指定的RecyclerView 上。
    • calculateDistanceToFinalSnap:複寫這個方法計算對齊到TargetView或容器指定點的距離,這是一個抽象方法,由子類本身實現,返回的是一個長度爲2的int 數組out,out[0]是x方向對齊要移動的距離,out[1]是y方向對齊要移動的距離。
    • calculateScrollDistance: 根據每一個方向給定的速度估算滑動的距離,用於Fling 操做。
    • findSnapView:提供一個指定的目標View 來對齊,抽象方法,須要子類實現
    • findTargetSnapPosition:提供一個用於對齊的Adapter 目標position,抽象方法,須要子類本身實現。
    • onFling:根據給定的x和 y 軸上的速度處理Fling。
  • 什麼是Fling操做ide

    • 手指在屏幕上滑動 RecyclerView而後鬆手,RecyclerView中的內容會順着慣性繼續往手指滑動的方向繼續滾動直到中止,這個過程叫作 Fling 。 Fling 操做從手指離開屏幕瞬間被觸發,在滾動中止時結束。

1.3 LinearSnapHelper類分析

  • LinearSnapHelper 使當前Item居中顯示,經常使用場景是橫向的RecyclerView,相似ViewPager效果,可是又能夠快速滑動(滑動多頁)。
  • 最簡單的使用就是,以下代碼函數

    • 幾行代碼就能夠用RecyclerView實現一個相似ViewPager的效果,而且效果還不錯。能夠快速滑動多頁,當前頁劇中顯示,而且顯示前一頁和後一頁的部分。
    private void initRecyclerView() {
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.HORIZONTAL);
        mRecyclerView.setLayoutManager(manager);
        LinearSnapHelper snapHelper = new LinearSnapHelper();
        snapHelper.attachToRecyclerView(mRecyclerView);
        SnapAdapter adapter = new SnapAdapter(this);
        mRecyclerView.setAdapter(adapter);
        adapter.addAll(getData());
    }

1.4 PagerSnapHelper類分析

  • PagerSnapHelper看名字可能就能猜到,使RecyclerView像ViewPager同樣的效果,每次只能滑動一頁(LinearSnapHelper支持快速滑動), PagerSnapHelper也是Item居中對齊。
  • 最簡單的使用就是,以下代碼工具

    private void initRecyclerView() {
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.HORIZONTAL);
        mRecyclerView.setLayoutManager(manager);
        PagerSnapHelper snapHelper = new PagerSnapHelper();
        snapHelper.attachToRecyclerView(mRecyclerView);
        SnapAdapter adapter = new SnapAdapter(this);
        mRecyclerView.setAdapter(adapter);
        adapter.addAll(getData());
    }

02.SnapHelper源碼分析

2.1 attachToRecyclerView入口方法

  • 經過attachToRecyclerView方法將SnapHelper attach 到RecyclerView,看一下這個方法的源代碼

    • 若是SnapHelper以前已經附着到此RecyclerView上,則不用進行任何操做
    • 若是SnapHelper以前附着的RecyclerView和如今的不一致,就將原來設置的回調所有remove或者設置爲null
    • 而後更新RecyclerView對象引用,Attach的RecyclerView不爲null,設置回調Callback,主要包括滑動的回調和Fling操做的回調,初始化一個Scroller 用於後面作滑動處理,而後調用snapToTargetExistingView
    • 大概流程就是:在attachToRecyclerView()方法中會清掉SnapHelper以前保存的RecyclerView對象的回調(若是有的話),對新設置進來的RecyclerView對象設置回調,而後初始化一個Scroller對象,最後調用snapToTargetExistingView()方法對SnapView進行對齊調整。
    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();
        }
    }
  • 接着看看setupCallbacks()源碼

    • 上面已經說了,滑動的回調和Fling操做的回調
    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }
  • 接着看看snapToTargetExistingView()方法

    • 這個方法用於第一次Attach到RecyclerView時對齊TargetView,或者當Scroll被觸發的時候和fling操做的時候對齊TargetView 。
    • 判斷RecyclerView 和LayoutManager是否爲null,接着調用findSnapView 方法來獲取須要對齊的目標View,注意:這是個抽象方法,須要子類實現
    • 經過calculateDistanceToFinalSnap 獲取x方向和y方向對齊須要移動的距離
    • 最後若是須要滾動的距離不是爲0,就調用smoothScrollBy方法使RecyclerView滾動相應的距離
    • 注意:RecyclerView.smoothScrollBy()這個方法的做用就是根據參數平滑滾動RecyclerView的中的ItemView相應的距離。
    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }
  • 而後來看一下mScrollListener監聽裏面作了什麼

    • 該滾動監聽器的實現很簡單,只是在正常滾動中止的時候調用了snapToTargetExistingView()方法對targetView進行滾動調整,以確保中止的位置是在對應的座標上,這就是RecyclerView添加該OnScrollListener的目的。
    • mScrolled爲true表示以前進行過滾動,newState爲SCROLL_STATE_IDLE狀態表示滾動結束停下來
    private final RecyclerView.OnScrollListener mScrollListener =
        new RecyclerView.OnScrollListener() {
            boolean mScrolled = false;
    
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                    mScrolled = false;
                    snapToTargetExistingView();
                }
            }
    
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                if (dx != 0 || dy != 0) {
                    mScrolled = true;
                }
            }
        };

2.2 SnapHelper的抽象方法

  • calculateDistanceToFinalSnap抽象方法

    • 計算最終對齊要移動的距離

      • 計算二個參數對應的 ItemView 當前的座標與須要對齊的座標之間的距離。該方法返回一個大小爲 2 的 int 數組,分別對應out[0] 爲 x 方向移動的距離,out[1] 爲 y 方向移動的距離。
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
        @NonNull View targetView);
  • findSnapView抽象方法

    • 找到要對齊的View

      • 該方法會找到當前 layoutManager 上最接近對齊位置的那個 view ,該 view 稱爲 SanpView ,對應的 position 稱爲 SnapPosition 。若是返回 null ,就表示沒有須要對齊的 View ,也就不會作滾動對齊調整。
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract View findSnapView(LayoutManager layoutManager);
  • findTargetSnapPosition抽象方法

    • 找到須要對齊的目標View的的Position。

      • 更加詳細一點說就是該方法會根據觸發 Fling 操做的速率(參數 velocityX 和參數 velocityY )來找到 RecyclerView 須要滾動到哪一個位置,該位置對應的 ItemView 就是那個須要進行對齊的列表項。咱們把這個位置稱爲 targetSnapPosition ,對應的 View 稱爲 targetSnapView 。若是找不到 targetSnapPosition ,就返回RecyclerView.NO_POSITION 。
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
        int velocityY);

2.3 onFling方法源碼分析

  • SnapHelper繼承了 RecyclerView.OnFlingListener,實現了onFling方法。

    • 獲取RecyclerView要進行fling操做須要的最小速率,爲啥呢?由於只有超過該速率,ItemView纔會有足夠的動力在手指離開屏幕時繼續滾動下去。
    @Override
    public boolean onFling(int velocityX, int velocityY) {
        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);
    }
  • 接着看看snapFromFling方法源代碼,就是經過該方法實現平滑滾動並使得在滾動中止時itemView對齊到目的座標位置

    • 首先layoutManager必須實現ScrollVectorProvider接口才能繼續往下操做
    • 而後經過createSnapScroller方法建立一個SmoothScroller,這個東西是一個平滑滾動器,用於對ItemView進行平滑滾動操做
    • 根據x和y方向的速度來獲取須要對齊的View的位置,須要子類實現
    • 最終經過 SmoothScroller 來滑動到指定位置
    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }
    
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
    
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
    
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }
    • 總結一下可知:snapFromFling()方法會先判斷layoutManager是否實現了ScrollVectorProvider接口,若是沒有實現該接口就不容許經過該方法作滾動操做。接下來就去建立平滑滾動器SmoothScroller的一個實例,layoutManager能夠經過該平滑滾動器來進行滾動操做。SmoothScroller須要設置一個滾動的目標位置,將經過findTargetSnapPosition()方法來計算獲得的targetSnapPosition給它,告訴滾動器要滾到這個位置,而後就啓動SmoothScroller進行滾動操做。
  • 接着看下createSnapScroller這個方法源碼

    • 先判斷layoutManager是否實現了ScrollVectorProvider這個接口,沒有實現該接口就不建立SmoothScroller
    • 這裏建立一個LinearSmoothScroller對象,而後返回給調用函數,也就是說,最終建立出來的平滑滾動器就是這個LinearSmoothScroller
    • 在建立該LinearSmoothScroller的時候主要考慮兩個方面:

      • 第一個是滾動速率,由calculateSpeedPerPixel()方法決定;
      • 第二個是在滾動過程當中,targetView即將要進入到視野時,將勻速滾動變換爲減速滾動,而後一直滾動目的座標位置,使滾動效果更真實,這是由onTargetFound()方法決定。
@Nullable
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
    if (!(layoutManager instanceof ScrollVectorProvider)) {
        return null;
    }
    return new LinearSmoothScroller(mRecyclerView.getContext()) {
        @Override
        protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
            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;
        }
    };
}

03.LinearSnapHelper源碼分析

3.1 LinearSnapHelper實現功能

  • LinearSnapHelper實現了SnapHelper,而且實現SnapHelper的三個抽象方法,從而讓ItemView滾動居中對齊。那麼具體怎麼作到呢?

3.2 calculateDistanceToFinalSnap()方法源碼

  • calculateDistanceToFinalSnap源碼以下所示

    • 若是是水平方向滾動的,則計算水平方向須要移動的距離,不然水平方向的移動距離爲0
    • 若是是豎直方向滾動的,則計算豎直方向須要移動的距離,不然豎直方向的移動距離爲0
    • distanceToCenter方法主要做用是:計算水平或者豎直方向須要移動的距離
    @Override
    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;
    }
  • 接着看看distanceToCenter方法

    • 計算對應的view的中心座標到RecyclerView中心座標之間的距離
    • 首先是找到targetView的中心座標
    • 接着也就是找到容器【RecyclerView】的中心座標
    • 兩個中心座標的差值就是targetView須要滾動的距離
    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        final int childCenter = helper.getDecoratedStart(targetView)
                + (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        return childCenter - containerCenter;
    }

3.3 findSnapView()方法源碼

  • 也就是找到要對齊的View

    • 根據layoutManager的佈局方式(水平佈局方式或者豎向佈局方式)區分計算,但最終都是經過findCenterView()方法來找snapView的。
    @Override
    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;
    }
  • 接着看看findCenterView方法源代碼

    • 查詢當前是否支持垂直滾動仍是橫向滾動
    • 循環LayoutManager的全部子元素,計算每一個 childView的中點距離Parent 的中點,找到距離最近的一個,就是須要居中對齊的目標View
    @Nullable
    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }
    
        View closestChild = null;
        final int center;
        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);
            int childCenter = helper.getDecoratedStart(child)
                    + (helper.getDecoratedMeasurement(child) / 2);
            int absDistance = Math.abs(childCenter - center);
    
            /** if child center is closer than previous closest, set it as closest  **/
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }

3.4 findTargetSnapPosition()方法源碼

  • LinearSnapHelper實現了SnapHelper,來看一下在findTargetSnapPosition操做了什麼

    • 若是是水平方向滾動的列表,估算出水平方向SnapHelper響應fling,對齊要滑動的position和當前position的差,不然,水平方向滾動的差值爲0
    • 若是是豎直方向滾動的列表,估算出豎直方向SnapHelper響應fling,對齊要滑動的position和當前position的差,不然,豎直方向滾動的差值爲0
    • 這個方法在計算targetPosition的時候把佈局方式和佈局方向都考慮進去了。佈局方式能夠經過layoutManager.canScrollHorizontally()/layoutManager.canScrollVertically()來判斷,佈局方向就經過RecyclerView.SmoothScroller.ScrollVectorProvider這個接口中的computeScrollVectorForPosition()方法來判斷。
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return RecyclerView.NO_POSITION;
        }
    
        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }
    
        final View currentView = findSnapView(layoutManager);
        if (currentView == null) {
            return RecyclerView.NO_POSITION;
        }
    
        final int currentPosition = layoutManager.getPosition(currentView);
        if (currentPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }
    
        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        // deltaJumps sign comes from the velocity which may not match the order of children in
        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
        // get the direction.
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd == null) {
            // cannot get a vector for the given position.
            return RecyclerView.NO_POSITION;
        }
    
        int vDeltaJump, hDeltaJump;
        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;
        }
    
        int targetPos = currentPosition + deltaJump;
        if (targetPos < 0) {
            targetPos = 0;
        }
        if (targetPos >= itemCount) {
            targetPos = itemCount - 1;
        }
        return targetPos;
    }

3.5 支持哪些LayoutManager

  • SnapHelper爲了適配layoutManager的各類狀況,特地要求只有實現了RecyclerView.SmoothScroller.ScrollVectorProvider接口的layoutManager才能使用SnapHelper進行輔助滾動對齊。官方提供的LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager都實現了這個接口,因此都支持SnapHelper。

3.6 OrientationHelper類

  • 如何建立OrientationHelper對象呢?以下所示

    • 好比,上面三個抽象方法都使用到了這個類,這個類是幹嗎的?
    • 計算位置的時候用的是OrientationHelper這個工具類,它是LayoutManager用於測量child的一個輔助類,能夠根據Layoutmanager的佈局方式和佈局方向來計算獲得ItemView的大小位置等信息。
    @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;
    }

3.7 estimateNextPositionDiffForFling計算偏移量

  • 以下所示

    • 首先,計算滾動的總距離,這個距離受到觸發fling時的速度的影響,獲得一個distances數組
    • 而後計算每一個ItemView的長度
    • 根據是橫向佈局仍是縱向佈局,來取對應佈局方向上的滾動距離
    • 總結大概流程就是:用滾動總距離除以itemview的長度,從而估算獲得須要滾動的item數量,此數值就是位置偏移量。而滾動距離是經過SnapHelper的calculateScrollDistance()方法獲得的,ItemView的長度是經過computeDistancePerChild()方法計算出來。
    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper, int velocityX, int velocityY) {
        int[] distances = calculateScrollDistance(velocityX, velocityY);
        float distancePerChild = computeDistancePerChild(layoutManager, helper);
        if (distancePerChild <= 0) {
            return 0;
        }
        int distance =
                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        return (int) Math.round(distance / distancePerChild);
    }

04.自定義SnapHelper類

4.1 業務需求

  • LinearSnapHelper 實現了居中對齊,那麼咱們只要更改一下對齊的規則就行,更改成開始對齊(計算目標 View到 Parent start 要滑動的距離),其餘的邏輯和 LinearSnapHelper 是同樣的。所以咱們選擇繼承 LinearSnapHelper
  • 大概流程

    • 重寫calculateDistanceToFinalSnap方法,計算SnapView當前位置與目標位置的距離
    • 寫findSnapView方法,找到當前時刻的SnapView
    • 能夠發現完成上面兩個方法就能夠呢,可是感受滑動效果不太好。滑動比較快時,會滾動很遠。在分析了上面的代碼可知,滾動速率,由createSnapScroller方法中的calculateSpeedPerPixel()方法決定。那麼是否是能夠修改一下速率就能夠解決問題呢。最後測試真的能夠,ok,完成了。
    • 固然還會發現滾動時候,會滑動多個item,若是相對item個數作限制,能夠在findTargetSnapPosition()方法中處理。
  • 代碼地址:https://github.com/yangchong2...

4.2 自定義helper類

  • 重寫calculateDistanceToFinalSnap方法

    • 這裏須要知道,在LinearSnapHelper中,out[0]和out[1]是經過distanceToCenter獲取的。那麼既然要設置開始對齊,那麼這裏須要建立distanceToStart方法
    @Nullable
    @Override
    public int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }
    
    private int distanceToStart(View targetView, OrientationHelper helper) {
        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
    }
  • 寫findSnapView方法,找到當前時刻的SnapView

    @Nullable
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof LinearLayoutManager) {
            if (layoutManager.canScrollHorizontally()) {
                return findStartView(layoutManager, getHorizontalHelper(layoutManager));
            } else {
                return findStartView(layoutManager, getVerticalHelper(layoutManager));
            }
        }
        return super.findSnapView(layoutManager);
    }
    
    private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (layoutManager instanceof LinearLayoutManager) {
            int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
            //須要判斷是不是最後一個Item,若是是最後一個則不讓對齊,以避免出現最後一個顯示不徹底。
            boolean isLastItem = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
                    == layoutManager.getItemCount() - 1;
            if (firstChild == RecyclerView.NO_POSITION || isLastItem) {
                return null;
            }
            View child = layoutManager.findViewByPosition(firstChild);
            if (helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2
                    && helper.getDecoratedEnd(child) > 0) {
                return child;
            } else {
                if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
                        == layoutManager.getItemCount() - 1) {
                    return null;
                } else {
                    return layoutManager.findViewByPosition(firstChild + 1);
                }
            }
        }
        return super.findSnapView(layoutManager);
    }
  • 修改滾動速率

    @Nullable
    protected LinearSmoothScroller createSnapScroller(final 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, RecyclerView.SmoothScroller.Action action) {
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
                final int dx;
                final int dy;
                if (snapDistances != null) {
                    dx = snapDistances[0];
                    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;
            }
        };
    }

關於其餘內容介紹

image

關於其餘內容介紹

01.關於博客彙總連接

02.關於個人博客

相關文章
相關標籤/搜索