SnapHelper原始碼深度解析

瀟湘劍雨發表於2019-01-19

目錄介紹

  • 01.SnapHelper簡單介紹
    • 1.1 SnapHelper作用
    • 1.2 SnapHelper類分析
    • 1.3 LinearSnapHelper類分析
    • 1.4 PagerSnapHelper類分析
  • 02.SnapHelper原始碼分析
    • 2.1 attachToRecyclerView入口方法
    • 2.2 SnapHelper的抽象方法
    • 2.3 onFling方法原始碼分析
  • 03.LinearSnapHelper原始碼分析
    • 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萬字],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.SnapHelper簡單介紹

1.1 SnapHelper作用

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

1.2 SnapHelper類分析

  • 查閱可知,SnapHelper繼承自RecyclerView.OnFlingListener,並且重寫了onFling方法,這個類程式碼並不多,下面會對重要方法一一解析。
    • 支援SnapHelper的RecyclerView.LayoutManager必須實現的方式:
      • RecyclerView.SmoothScroller.ScrollVectorProvider介面
      • 或者自己實現onFling(int,int)方法手動處理邏輯。
  • SnapHelper類重要的方法
    • 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操作
    • 手指在螢幕上滑動 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()方法中處理。
  • 程式碼地址:github.com/yangchong21…

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.關於我的部落格

相關文章