SnapHelper源碼深度解析

目錄介紹

  • 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萬字],轉載請註明出處,謝謝!
  • 連接地址:https://github.com/yangchong211/YCBlogs
  • 若是以爲好,能夠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監聽裏面作了什麼php

    • 該滾動監聽器的實現很簡單,只是在正常滾動中止的時候調用了snapToTargetExistingView()方法對targetView進行滾動調整,以確保中止的位置是在對應的座標上,這就是RecyclerView添加該OnScrollListener的目的。
    • mScrolled爲true表示以前進行過滾動,newState爲SCROLL_STATE_IDLE狀態表示滾動結束停下來git

      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對齊到目的座標位置github

    • 首先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這個方法源碼segmentfault

    • 先判斷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方法主要做用是:計算水平或者豎直方向須要移動的距離markdown

      @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方法源代碼ide

    • 查詢當前是否支持垂直滾動仍是橫向滾動
    • 循環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/yangchong211/YCBanner

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.關於個人博客

相關文章
相關標籤/搜索