RecycerView擴(kuò)展SnapHepler源碼分析

SnapHelper作用

SnapHelper:翻譯過來為卡片幫助者,常見的有ViewPager2,Banner的卡片滑動效果都是借助RecyclerView和SnapHeler來實(shí)現(xiàn)。

SnapHelper通過綁定RecyclerViewonScrollListeneronFlingListener來監(jiān)聽RecyclerView的滑動過程,從而實(shí)現(xiàn)一個卡片滑動的效果。

public class RecyclerView extends ViewGroup implements ScrollingView,
      NestedScrollingChild2, NestedScrollingChild3 {
   //  ....
  public abstract static class OnScrollListener {
      // RecyclerView滑動狀態(tài)改變時調(diào)用,如開始、結(jié)束滑動時調(diào)用
      public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
      }
      // RecyclerView滑動時調(diào)用
      public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
      }
  }

  public abstract static class OnFlingListener {
      // RecyclerView在開始Fling時調(diào)用
      public abstract boolean onFling(int velocityX, int velocityY);
  }

}

SnapHeler有三個抽象方法:calculateDistanceToFinalSnap、findSnapView、findTargetSnapPosition

抽象方法 作用
findSnapView scrolling時將要滑到的View
calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager,View targetView) 計算到目標(biāo)View還需要滑動的距離
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY) fling時RecyclerView滑動的位置,返回RecyclerView.NO_POSITION表示支持fling

這里的三個抽象方法,是繼承SnapHelper時必須繼承實(shí)現(xiàn)的方法,下面講解這三個方法分別代表的作用。
findSnapView:RecyclerView正常滑動時,下一個達(dá)到的目標(biāo)View。
calculateDistanceToFinalSnap:RecyclerView正?;瑒訒r,在達(dá)到目標(biāo)View時,還要互動多少距離。
findTargetSnapPosition:RecyclerView在Fling滑動時,返回能滑動到的位置。

這個幫助類,是怎樣通過設(shè)置RecyclerView的監(jiān)聽來達(dá)到卡片滑動的效果的呢?這就要分析其源碼。

SnapHelper源碼分析

入口attachToRecyclerView

public abstract class SnapHelper extends RecyclerView.OnFlingListener {

  // 通過外界綁定RecyclerView,設(shè)置監(jiān)聽
  public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
          throws IllegalStateException {
      // 如果和上一個設(shè)置的RecyclerView一樣,則不重新設(shè)置監(jiān)聽
      if (mRecyclerView == recyclerView) {
          return;
      }
      // 將上一個的RecyclerView解除綁定
      if (mRecyclerView != null) {
          destroyCallbacks(); 
      }
      // 給現(xiàn)在的RecyclerView設(shè)置OnScrollListener和OnFlingListener監(jiān)聽
      mRecyclerView = recyclerView;
      if (mRecyclerView != null) {
          setupCallbacks();
          mGravityScroller = new Scroller(mRecyclerView.getContext(),
                  new DecelerateInterpolator());
          snapToTargetExistingView();
      }
  }

  // 設(shè)置現(xiàn)在的RecyclerView的OnScrollListener和OnFlingListener監(jiā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);
  }

  // 解除上一個RecyclerView的OnScrollListener和OnFlingListener監(jiān)聽
  private void destroyCallbacks() {
      mRecyclerView.removeOnScrollListener(mScrollListener);
      mRecyclerView.setOnFlingListener(null);
  } 

attachToRecyclerView方法很簡單,就是給RecyclerView設(shè)置OnScrollListenerOnFlingListener監(jiān)聽。接下我們看看兩個監(jiān)聽實(shí)例,在RecyclerView滑動的時候做了什么。

SnapHelper的監(jiān)聽實(shí)例

public abstract class SnapHelper extends RecyclerView.OnFlingListener {

  private final RecyclerView.OnScrollListener mScrollListener =
          new RecyclerView.OnScrollListener() {
              // RecyclerView是否在滑動變量
              boolean mScrolled = false;
               // 滑動狀態(tài)發(fā)生改變調(diào)用
              @Override
              public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                  super.onScrollStateChanged(recyclerView, newState);
                  // RecyclerView已經(jīng)結(jié)束滑動
                  if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                      mScrolled = false;
                      snapToTargetExistingView(); // 調(diào)用snapToTargetExistingView方法
                  }
              }

              @Override
              public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                  // 滑動時,設(shè)置滑動變量為ture
                  if (dx != 0 || dy != 0) {
                      mScrolled = true;
                  }
              }
          };

  // 結(jié)束滑動時調(diào)用
  void snapToTargetExistingView() {
      // 判空處理
      if (mRecyclerView == null) {
          return;
      }
      RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
      if (layoutManager == null) {
          return;
      }
      // 根據(jù)layoutManager調(diào)用findSnapView拿到目標(biāo)view
      View snapView = findSnapView(layoutManager);
      if (snapView == null) {
          return;
      }
      // 根據(jù)layoutManager和目標(biāo)View計算還要滑動的距離
      int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
      // RecyclerView繼續(xù)滑動達(dá)到目標(biāo)View的位置
      if (snapDistance[0] != 0 || snapDistance[1] != 0) {
          mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
      }
  }
   
  @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;
      }
     // 觸發(fā)Fling的最小力度
      int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
      // 如果達(dá)到觸發(fā)Fling條件則,調(diào)用snapFromFling方法
      // 這里返回true表示由自己處理Fling滑動,返回false則由RecyclerView的fling方法處理滑動。
      return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
              && snapFromFling(layoutManager, velocityX, velocityY);
  }
  // 在Fling開始時調(diào)用
  private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
          int velocityY) {
      // 判空處理,返回false由RecyclerView處理滑動
      if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
          return false;
      }

      RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
      if (smoothScroller == null) {
          return false;
      }
      // Fling滑動的位置
      int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
      // 如果findTargetSnapPosition返回的位置是RecyclerView.NO_POSITION,則由RecyclerView處理Fling
      if (targetPosition == RecyclerView.NO_POSITION) {
          return false;
      }
      // 自己根據(jù)positon處理Fling,并返回true
      smoothScroller.setTargetPosition(targetPosition);
      layoutManager.startSmoothScroll(smoothScroller);
      return true;
  }

通過源碼,SnapHelper在RecyclerView停止滑動的時候,通過調(diào)用findSnapViewcalculateDistanceToFinalSnap來得到目標(biāo)View,并讓RecyclerView繼續(xù)滑動達(dá)到目標(biāo)View的位置。
在RecyclerView在Fling開始的,通過調(diào)用findTargetSnapPosition方法來判斷是否自己處理Fling事件。如果不想自己處理Fling事件,可以在findTargetSnapPosition方法返回RecyclerView. NO_POSITION

看來SnapHelper只是一個基礎(chǔ)類,并沒有幫助我們做很多事。但谷歌已經(jīng)為我們實(shí)現(xiàn)兩個實(shí)例類分別是:LinearSnapHelper,PagerSnapHelper

LinearSnapHelper源碼分析

LinearSnapHelper實(shí)現(xiàn)的卡片效果是:使目標(biāo)View能夠居中顯示,效果圖如下:

SVID_20210124_214510_1.gif

LinearSnapHelper.findSnapView尋找目標(biāo)View
LinearSnapHelper尋找目標(biāo)View就是計算View的中心點(diǎn)位置RecyclerView的中心位置最近的一個View。

  @Override
  public View findSnapView(RecyclerView.LayoutManager layoutManager) {
      // 通過findCenterView方法找到目標(biāo)View
      if (layoutManager.canScrollVertically()) {
          return findCenterView(layoutManager, getVerticalHelper(layoutManager));
      } else if (layoutManager.canScrollHorizontally()) {
          return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
      }
      return null;
  }

  // 返回最靠近RecyclerView中心點(diǎn)的View
  private View findCenterView(RecyclerView.LayoutManager layoutManager,
          OrientationHelper helper) {
      // 遍歷屏幕可見View的數(shù)量
      int childCount = layoutManager.getChildCount();
      if (childCount == 0) {
          return null;
      }

      // 最靠近中心點(diǎn)的View
      View closestChild = null;  
      // recyclerView的中點(diǎn)
      final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 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);

          // 如果比前面的View的距離小,則更改目標(biāo)View
          if (absDistance < absClosest) {
              absClosest = absDistance;
              closestChild = child;
          }
      }
      return closestChild;
  }

因為LinearSnapHelper的效果使目標(biāo)View居中顯示,所以選中的目標(biāo)View就是離父類中心點(diǎn)最近的View。

LinearSnapHelper.calculateDistanceToFinalSnap計算需要滑動的距離
需要滑動的距離,就是目標(biāo)View的中心點(diǎn)到RecyclerView中心點(diǎn)的距離。

  @Override
  public int[] calculateDistanceToFinalSnap(
          @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
      // 通過distanceToCenter方法計算x,y軸需要滑動的距離
      int[] out = new int[2];
      if (layoutManager.canScrollHorizontally()) {
          out[0] = distanceToCenter(targetView,
                  getHorizontalHelper(layoutManager));
      } else {
          out[0] = 0;
      }

      if (layoutManager.canScrollVertically()) {
          out[1] = distanceToCenter(targetView,
                  getVerticalHelper(layoutManager));
      } else {
          out[1] = 0;
      }
      return out;
  }

  private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) {
      // 目標(biāo)View的中心位置
      final int childCenter = helper.getDecoratedStart(targetView)
              + (helper.getDecoratedMeasurement(targetView) / 2);
      // RecyclerView的中心位置
      final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
      // 目標(biāo)View中心位置到控件中心位置的距離
      return childCenter - containerCenter;
  }

代碼也很簡單,就不做講解。

LinearSnapHelper.findTargetSnapPosition在Fling時最后的位置

  @Override
  public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
          int velocityY) {
       // 谷歌提供的三個layoutManager都實(shí)現(xiàn)了,用來判斷布局的方向
      if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
          return RecyclerView.NO_POSITION;
      }
      // 沒有數(shù)據(jù)
      final int itemCount = layoutManager.getItemCount();
      if (itemCount == 0) {
          return RecyclerView.NO_POSITION;
      }
      // 未fling前,離容器中心點(diǎn)最近的View
      final View currentView = findSnapView(layoutManager);
      if (currentView == null) {
          return RecyclerView.NO_POSITION;
      }

       // 未fling前,目標(biāo)View的實(shí)際位置
      final int currentPosition = layoutManager.getPosition(currentView);
      if (currentPosition == RecyclerView.NO_POSITION) {
          return RecyclerView.NO_POSITION;
      }

      RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
              (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;

      // 到最后一個View的布局方向
      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()) {
          // 計算水平Fling可以橫跨多少個位置
          hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                  getHorizontalHelper(layoutManager), velocityX, 0);
          if (vectorForEnd.x < 0) {
              hDeltaJump = -hDeltaJump;
          }
      } else {
          // 垂直滑動
          hDeltaJump = 0;
      }
       // 垂直滑動
      if (layoutManager.canScrollVertically()) {
          // 計算垂直Fling可以橫跨多少個位置
          vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                  getVerticalHelper(layoutManager), 0, velocityY);
          if (vectorForEnd.y < 0) {
              vDeltaJump = -vDeltaJump;
          }
      } else {
          // 水平滑動
          vDeltaJump = 0;
      }

       // 根據(jù)可滑動方向,選擇變量
      int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;

      if (deltaJump == 0) {
          return RecyclerView.NO_POSITION;
      }

     // Fling最后的Position
      int targetPos = currentPosition + deltaJump;
      if (targetPos < 0) {
          targetPos = 0;
      }
      if (targetPos >= itemCount) {
          targetPos = itemCount - 1;
      }
      return targetPos;
  }

findTargetSnapPosition返回的是Fling最后停留的位置,計算的方式是通過當(dāng)前目標(biāo)View的position+Fling橫跨的View的數(shù)量。由上面代碼可以看到estimateNextPositionDiffForFling就是返回Fling后橫跨View數(shù)量的方法。

  private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
          OrientationHelper helper, int velocityX, int velocityY) {
      // 根據(jù)加速度計算滑動的距離
      int[] distances = calculateScrollDistance(velocityX, velocityY);
      // 根據(jù)在屏幕兩邊的距離/屏幕的View數(shù),得到平均的View的長度
      float distancePerChild = computeDistancePerChild(layoutManager, helper);
      if (distancePerChild <= 0) {
          return 0;
      }
      int distance =
              Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
       // Fling的距離 / 平均View的長度
      return (int) Math.round(distance / distancePerChild);
  }

PagerSnapHelper

PagerSnapHelper的滑動效果也是居中顯示,不過一次只能滑動一頁。實(shí)現(xiàn)的效果如下。

SVID_20210127_093933_1.gif

PagerSnapHelper的findSnapView、calculateDistanceToFinalSnap
PagerSnapHelper的findSnapViewcalculateDistanceToFinalSnapLinearSnapHelper的一樣,都是尋找離中心點(diǎn)最近的View和計算兩中心點(diǎn)的距離,這里不做分析。

PagerSnapHelper的findTargetSnapPosition計算Fling的最后位置
我們從前面的效果圖可以知道,就是Fling時也最多滑動一頁,所以我們看看代碼如何實(shí)現(xiàn)。

  @Override
  public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
          int velocityY) {
      final int itemCount = layoutManager.getItemCount();
      if (itemCount == 0) {
          return RecyclerView.NO_POSITION;
      }

      final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
      if (orientationHelper == null) {
          return RecyclerView.NO_POSITION;
      }

     // 離中心點(diǎn)最近的左邊View
      View closestChildBeforeCenter = null;
      int distanceBefore = Integer.MIN_VALUE;
      // 離中心點(diǎn)最近的右邊View
      View closestChildAfterCenter = null;
      int distanceAfter = Integer.MAX_VALUE;

      // 遍歷屏幕中的view,尋找離中心點(diǎn)最近的左右view
      final int childCount = layoutManager.getChildCount();
      for (int i = 0; i < childCount; i++) {
          final View child = layoutManager.getChildAt(i);
          if (child == null) {
              continue;
          }
           // 計算子view到中心點(diǎn)的距離
          final int distance = distanceToCenter(child, orientationHelper);
          //  找到左邊離中心點(diǎn)最近的子View,如果是已經(jīng)有View的中心點(diǎn)等于控件的中心點(diǎn),則這個View為closestChildBeforeCenter,且
          if (distance <= 0 && distance > distanceBefore) {
              // Child is before the center and closer then the previous best
              distanceBefore = distance;
              closestChildBeforeCenter = child;
          }
          // 找到右邊離中心點(diǎn)最近的子View
          if (distance >= 0 && distance < distanceAfter) {
              // Child is after the center and closer then the previous best
              distanceAfter = distance;
              closestChildAfterCenter = child;
          }
      }

      // 判斷Fling的方向,true則滑右邊,false為滑左邊
      final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
      if (forwardDirection && closestChildAfterCenter != null) {
          return layoutManager.getPosition(closestChildAfterCenter);
      } else if (!forwardDirection && closestChildBeforeCenter != null) {
          return layoutManager.getPosition(closestChildBeforeCenter);
      }

       // 這種就是一個子View占滿控件,需要上下切換View的情況。
      View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
      if (visibleView == null) {
          return RecyclerView.NO_POSITION;
      }
      int visiblePosition = layoutManager.getPosition(visibleView);
      int snapToPosition = visiblePosition
              + (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);

      if (snapToPosition < 0 || snapToPosition >= itemCount) {
          return RecyclerView.NO_POSITION;
      }
      return snapToPosition;
  }

所以為什么在Fling后只會切換一頁,原因是它會找出屏幕中離中心點(diǎn)最近的左右兩個View,然后根據(jù)滑動方向決定使用那個view的position。

總結(jié)

SnapHelper的作用在RecyclerView停止滑動的時候,對某個特定的View進(jìn)行位置的滑動調(diào)整,并可以自定義RecyclerView在Fling的距離,F(xiàn)ling的最后的一個位置。
原理大致是:SnapHelper對RecyclerView進(jìn)行OnScrollListener和OnFlingListener監(jiān)聽。OnScrollListener監(jiān)聽在滑動結(jié)束后,會依次調(diào)用findSnapViewcalculateDistanceToFinalSnap分別得到目標(biāo)View和需要滑動的距離。OnFlingListener監(jiān)聽在fling方法時,調(diào)用findTargetSnapPosition得到Fling后的距離,并進(jìn)行滑動Fling。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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