RecycerView扩展SnapHepler源码分析

SnapHelper作用

SnapHelper:翻译过来为卡片帮助者,常见的有ViewPager2,Banner的卡片滑动效果都是借助RecyclerView和SnapHeler来实现。

SnapHelper通过绑定RecyclerViewonScrollListeneronFlingListener来监听RecyclerView的滑动过程,从而实现一个卡片滑动的效果。

public class RecyclerView extends ViewGroup implements ScrollingView,
      NestedScrollingChild2, NestedScrollingChild3 {
   //  ....
  public abstract static class OnScrollListener {
      // RecyclerView滑动状态改变时调用,如开始、结束滑动时调用
      public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
      }
      // RecyclerView滑动时调用
      public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
      }
  }

  public abstract static class OnFlingListener {
      // RecyclerView在开始Fling时调用
      public abstract boolean onFling(int velocityX, int velocityY);
  }

}

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

抽象方法 作用
findSnapView scrolling时将要滑到的View
calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager,View targetView) 计算到目标View还需要滑动的距离
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY) fling时RecyclerView滑动的位置,返回RecyclerView.NO_POSITION表示支持fling

这里的三个抽象方法,是继承SnapHelper时必须继承实现的方法,下面讲解这三个方法分别代表的作用。
findSnapView:RecyclerView正常滑动时,下一个达到的目标View。
calculateDistanceToFinalSnap:RecyclerView正常滑动时,在达到目标View时,还要互动多少距离。
findTargetSnapPosition:RecyclerView在Fling滑动时,返回能滑动到的位置。

这个帮助类,是怎样通过设置RecyclerView的监听来达到卡片滑动的效果的呢?这就要分析其源码。

SnapHelper源码分析

入口attachToRecyclerView

public abstract class SnapHelper extends RecyclerView.OnFlingListener {

  // 通过外界绑定RecyclerView,设置监听
  public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
          throws IllegalStateException {
      // 如果和上一个设置的RecyclerView一样,则不重新设置监听
      if (mRecyclerView == recyclerView) {
          return;
      }
      // 将上一个的RecyclerView解除绑定
      if (mRecyclerView != null) {
          destroyCallbacks(); 
      }
      // 给现在的RecyclerView设置OnScrollListener和OnFlingListener监听
      mRecyclerView = recyclerView;
      if (mRecyclerView != null) {
          setupCallbacks();
          mGravityScroller = new Scroller(mRecyclerView.getContext(),
                  new DecelerateInterpolator());
          snapToTargetExistingView();
      }
  }

  // 设置现在的RecyclerView的OnScrollListener和OnFlingListener监听
  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监听
  private void destroyCallbacks() {
      mRecyclerView.removeOnScrollListener(mScrollListener);
      mRecyclerView.setOnFlingListener(null);
  } 

attachToRecyclerView方法很简单,就是给RecyclerView设置OnScrollListenerOnFlingListener监听。接下我们看看两个监听实例,在RecyclerView滑动的时候做了什么。

SnapHelper的监听实例

public abstract class SnapHelper extends RecyclerView.OnFlingListener {

  private final RecyclerView.OnScrollListener mScrollListener =
          new RecyclerView.OnScrollListener() {
              // RecyclerView是否在滑动变量
              boolean mScrolled = false;
               // 滑动状态发生改变调用
              @Override
              public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                  super.onScrollStateChanged(recyclerView, newState);
                  // RecyclerView已经结束滑动
                  if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                      mScrolled = false;
                      snapToTargetExistingView(); // 调用snapToTargetExistingView方法
                  }
              }

              @Override
              public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                  // 滑动时,设置滑动变量为ture
                  if (dx != 0 || dy != 0) {
                      mScrolled = true;
                  }
              }
          };

  // 结束滑动时调用
  void snapToTargetExistingView() {
      // 判空处理
      if (mRecyclerView == null) {
          return;
      }
      RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
      if (layoutManager == null) {
          return;
      }
      // 根据layoutManager调用findSnapView拿到目标view
      View snapView = findSnapView(layoutManager);
      if (snapView == null) {
          return;
      }
      // 根据layoutManager和目标View计算还要滑动的距离
      int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
      // RecyclerView继续滑动达到目标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;
      }
     // 触发Fling的最小力度
      int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
      // 如果达到触发Fling条件则,调用snapFromFling方法
      // 这里返回true表示由自己处理Fling滑动,返回false则由RecyclerView的fling方法处理滑动。
      return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
              && snapFromFling(layoutManager, velocityX, velocityY);
  }
  // 在Fling开始时调用
  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;
      }
      // 自己根据positon处理Fling,并返回true
      smoothScroller.setTargetPosition(targetPosition);
      layoutManager.startSmoothScroll(smoothScroller);
      return true;
  }

通过源码,SnapHelper在RecyclerView停止滑动的时候,通过调用findSnapViewcalculateDistanceToFinalSnap来得到目标View,并让RecyclerView继续滑动达到目标View的位置。
在RecyclerView在Fling开始的,通过调用findTargetSnapPosition方法来判断是否自己处理Fling事件。如果不想自己处理Fling事件,可以在findTargetSnapPosition方法返回RecyclerView. NO_POSITION

看来SnapHelper只是一个基础类,并没有帮助我们做很多事。但谷歌已经为我们实现两个实例类分别是:LinearSnapHelper,PagerSnapHelper

LinearSnapHelper源码分析

LinearSnapHelper实现的卡片效果是:使目标View能够居中显示,效果图如下:

SVID_20210124_214510_1.gif

LinearSnapHelper.findSnapView寻找目标View
LinearSnapHelper寻找目标View就是计算View的中心点位置RecyclerView的中心位置最近的一个View。

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

  // 返回最靠近RecyclerView中心点的View
  private View findCenterView(RecyclerView.LayoutManager layoutManager,
          OrientationHelper helper) {
      // 遍历屏幕可见View的数量
      int childCount = layoutManager.getChildCount();
      if (childCount == 0) {
          return null;
      }

      // 最靠近中心点的View
      View closestChild = null;  
      // recyclerView的中点
      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的距离小,则更改目标View
          if (absDistance < absClosest) {
              absClosest = absDistance;
              closestChild = child;
          }
      }
      return closestChild;
  }

因为LinearSnapHelper的效果使目标View居中显示,所以选中的目标View就是离父类中心点最近的View。

LinearSnapHelper.calculateDistanceToFinalSnap计算需要滑动的距离
需要滑动的距离,就是目标View的中心点到RecyclerView中心点的距离。

  @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) {
      // 目标View的中心位置
      final int childCenter = helper.getDecoratedStart(targetView)
              + (helper.getDecoratedMeasurement(targetView) / 2);
      // RecyclerView的中心位置
      final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
      // 目标View中心位置到控件中心位置的距离
      return childCenter - containerCenter;
  }

代码也很简单,就不做讲解。

LinearSnapHelper.findTargetSnapPosition在Fling时最后的位置

  @Override
  public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
          int velocityY) {
       // 谷歌提供的三个layoutManager都实现了,用来判断布局的方向
      if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
          return RecyclerView.NO_POSITION;
      }
      // 没有数据
      final int itemCount = layoutManager.getItemCount();
      if (itemCount == 0) {
          return RecyclerView.NO_POSITION;
      }
      // 未fling前,离容器中心点最近的View
      final View currentView = findSnapView(layoutManager);
      if (currentView == null) {
          return RecyclerView.NO_POSITION;
      }

       // 未fling前,目标View的实际位置
      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;
      }

       // 根据可滑动方向,选择变量
      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最后停留的位置,计算的方式是通过当前目标View的position+Fling横跨的View的数量。由上面代码可以看到estimateNextPositionDiffForFling就是返回Fling后横跨View数量的方法。

  private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
          OrientationHelper helper, int velocityX, int velocityY) {
      // 根据加速度计算滑动的距离
      int[] distances = calculateScrollDistance(velocityX, velocityY);
      // 根据在屏幕两边的距离/屏幕的View数,得到平均的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的滑动效果也是居中显示,不过一次只能滑动一页。实现的效果如下。

SVID_20210127_093933_1.gif

PagerSnapHelper的findSnapView、calculateDistanceToFinalSnap
PagerSnapHelper的findSnapViewcalculateDistanceToFinalSnapLinearSnapHelper的一样,都是寻找离中心点最近的View和计算两中心点的距离,这里不做分析。

PagerSnapHelper的findTargetSnapPosition计算Fling的最后位置
我们从前面的效果图可以知道,就是Fling时也最多滑动一页,所以我们看看代码如何实现。

  @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;
      }

     // 离中心点最近的左边View
      View closestChildBeforeCenter = null;
      int distanceBefore = Integer.MIN_VALUE;
      // 离中心点最近的右边View
      View closestChildAfterCenter = null;
      int distanceAfter = Integer.MAX_VALUE;

      // 遍历屏幕中的view,寻找离中心点最近的左右view
      final int childCount = layoutManager.getChildCount();
      for (int i = 0; i < childCount; i++) {
          final View child = layoutManager.getChildAt(i);
          if (child == null) {
              continue;
          }
           // 计算子view到中心点的距离
          final int distance = distanceToCenter(child, orientationHelper);
          //  找到左边离中心点最近的子View,如果是已经有View的中心点等于控件的中心点,则这个View为closestChildBeforeCenter,且
          if (distance <= 0 && distance > distanceBefore) {
              // Child is before the center and closer then the previous best
              distanceBefore = distance;
              closestChildBeforeCenter = child;
          }
          // 找到右边离中心点最近的子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后只会切换一页,原因是它会找出屏幕中离中心点最近的左右两个View,然后根据滑动方向决定使用那个view的position。

总结

SnapHelper的作用在RecyclerView停止滑动的时候,对某个特定的View进行位置的滑动调整,并可以自定义RecyclerView在Fling的距离,Fling的最后的一个位置。
原理大致是:SnapHelper对RecyclerView进行OnScrollListener和OnFlingListener监听。OnScrollListener监听在滑动结束后,会依次调用findSnapViewcalculateDistanceToFinalSnap分别得到目标View和需要滑动的距离。OnFlingListener监听在fling方法时,调用findTargetSnapPosition得到Fling后的距离,并进行滑动Fling。

你可能感兴趣的:(RecycerView扩展SnapHepler源码分析)