RecyclerView之PagerSnapHelper原理解析(二)

通过RecyclerView之SnapHelper原理解析(一) 这篇文章可知只要实现RecyclerView.OnFlingListener接口,并将该接口的fling方法返回true就可以简单的将RecyclerView作为ViewPager来使用,让RecycerView分页滑动,原理就是根据滚动的距离/recyerView的高度来计算滚动的当前页数。下面就来说说Android 提供的另外一个库用PageSnapHelper是怎么工作的。

SnapHepler是什么?该组件本质上仍然就是一个RecyclerView.OnFlingListener

public abstract class SnapHelper extends RecyclerView.OnFlingListener

该类是个抽象类,有两个实现类LinearSnapHelperPagerSnapHelper!关于PageSnapHelper,官方一句解释挺到位:
PagerSnapHelper can help achieve a similar behavior to ViewPager.,就是让RecyclerView能像ViewPager一样工作

所以RecyclerView之SnapHelper原理解析(一) 费死了劲的写了怎么实现RecyclerView翻页滚动的效果,用PageSnapHelper两行代码的事儿:

 PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
 pagerSnapHelper.attachToRecyclerView(recyclerView);

但是PagerSnapHelper并没有告诉我们当前页是第几页,所以需要额外的处理:思路就是预先知道当前页是第几页,只有等滚动结束的时候才可以知道。所以在监听滚动功能添加下面代码就可以(注意一个大前提就是本例子中recyclerView的高度和itemView的高度是一样的,且RecyclerView为竖直布局,即LinearLayoutManager):

  
final PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
 pagerSnapHelper.attachToRecyclerView(recyclerView);
  recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
      private int currentPage = -1;
      @Override
      public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
          if(newState== RecyclerView.SCROLL_STATE_IDLE){//如果滚动结束
              View snapView = pagerSnapHelper.findSnapView(linearLayoutManager);
              int currentPageIndex = linearLayoutManager.getPosition(snapView);
              if(currentPage!=currentPageIndex){//防止重复提示
                  currentPage = currentPageIndex;
                  Toast.makeText(MainActivity.this, "当前是第" + currentPageIndex + "页", Toast.LENGTH_SHORT).show();
              }
          }
      }
  });

简单吧?核心代码仍然是两行:
首先通过pagerSnapHelper.findSnapView(linearLayoutManager)来查到snapView
其次通过linearLayoutManagergetPosition(view)方法知道当前view的位置,也就是currentPageindex


来看看pagerSnapHelper的findSnapView都做了什么:

   public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        }
        //省略水平布局
        return null;
    }
 private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
       
        View closestChild = null;
        final int center;
        if (layoutManager.getClipToPadding()) {
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            //RecyclerView的中心线
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;

        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            //itemView的中心线
            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;
    }

所以从上面的代码可以看出findSnapView的主要作用就是查找itemView中心距离RecyclerView中心最近的那一个View。有两种情况,比如
在这里插入图片描述
对于上图这种,上面itemView的中心点距离RecycerView的中心点距离最近(屏幕中间的那条线),所以上面的itemView就是findSnapView返回的snapView.此时松开手指的话,上面的itemView就会向下自动滑动,也就是实现了上一页的效果。同理,下图中下面itemView的中心点距离RecycerView的中心点距离最近,所以下面的itemView就是查找的snapView.此时松开手指的话,下面的itemView就会向上自动滑动,也就是实现了下一页的效果:
在这里插入图片描述

所以实现上一页或者下一页的滚动的距离就是 itemView中心位置和RecyclerView的中心位置的距离(该结论详见calculateDistanceToFinalSnap方法)!现在就来看看具体的滚动逻辑,
从其父类SnapHelperSnapHelper内置了RecyclerView.OnScrollListener,且当RecyclerView滚动结束的时候该listener会调用snapToTargetExistingView从其名字可以看出就是在滚动结束的时候,如果还没有滚动到新的一页,就将targetView自动滚动到具体的位置,针对上面两幅图来看就是实现手指离开自动滚动上一页或者下一页的功能,具体看代码:

  public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
     super.onScrollStateChanged(recyclerView, newState);
     if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {//如果滚动结束
         mScrolled = false;
         //是的snapView自动滚动到下一页或者上一页
         snapToTargetExistingView();
     }
 }


 void snapToTargetExistingView() {
    
        View snapView = findSnapView(layoutManager);
       
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);     
        }
    }

看看snapToTargetExistingView的逻辑,很简单吧。就是将查找到的snapView通过calculateDistanceToFinalSnap抽象方法计算出剩余要滚动的距离,然后调用RecyclerView的smoothScrollBy滚动即可。在PageSnapHelper类实现了calculateDistanceToFinalSnap

public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView) {
        int[] out = new int[2];
       //删除水平滚动逻辑
       //判断是否可以滚动
        if (layoutManager.canScrollVertically()) {
           //计算snapView的中心点到RecyclerView中心点的距离
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

   //计算snapView的中心点到RecyclerView中心点的距离
  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;
    }

可以看出对于PageSnapHelpersnapToTargetExistingView就是找到snapView的中心距离RecylerView的中心的距离distanceToCenter,然后调用smoothScrollBy滚动即可。对于父类SnapHelper来说snapToTargetExistingView的用意就是找到snapView,然后计算出snapView到达目标位置的距离然后滚动之,使得snapView达到目标位置,而这个snapView就是指定要滚动到目标位置的那个View(当然在这里是距离目标位置最近的view)


以上说的都是在RecyclerView滚动结束后调用snapToTargetExistingView方法,使得snapView与目标位置对齐达到翻页的效果。
PagerSnapHelper的移动规则是每次滑动将距离中心位置最近的item移动到RecyclerView中心位置

但是文章开头就说过SnapHelper本质就是一个OnFlingListener接口,所以之所以能达到翻页的滚动效果还是因为OnFlingListener接口接管了RecyclerViewfling惯性滚动效果,这是前提snapToTargetExistingView能工作的前提,所以看看onFling方法都做了什么:

 public boolean onFling(int velocityX, int velocityY) {
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
 }


private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
        int velocityY) {

    //根据惯性速度velocityY知道滚动停止时的位置Position
    int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);

    //设置目标位置
    smoothScroller.setTargetPosition(targetPosition);
    //开始滚动
    layoutManager.startSmoothScroll(smoothScroller);
    return true;
}

可以看出SnapHelper处理惯性滚动逻辑很简单,就竖直滚动来说,首先根据layoutManager+ velocityY两个参数查找到惯性滚动要结束的位置targetPosition,如果找到的话就通过layoutManager.startSmoothScroll开始滚动的目标位置。 需要注意的是findTargetSnapPositionSnapHelper是一个抽象方法,所以我们来看看PageSnapHelper是怎么实现的(以竖直滚动为例,提出了水平滚动的代码):

 @Override
 public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
          int velocityY) {

      final int itemCount = layoutManager.getItemCount();
       //findStartView获取的就是距离RecyclerView中心点最近的view
      View mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));

     
      final int centerPosition = layoutManager.getPosition(mStartMostChildView);

      //velocityY>0下一页 <0 上一页
      final boolean forwardDirection = velocityY > 0;
     
      boolean reverseLayout = false;
      if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
          RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                  (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
          //判断滚动的方向
          PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
          if (vectorForEnd != null) {
              reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
          }
      }
      return reverseLayout
              ? (forwardDirection ? centerPosition - 1 : centerPosition)
              : (forwardDirection ? centerPosition + 1 : centerPosition);
  }



  private View findStartView(RecyclerView.LayoutManager layoutManager,
                       OrientationHelper helper) {
       int childCount = layoutManager.getChildCount();
       View closestChild = null;
      int startest = Integer.MAX_VALUE;

      for (int i = 0; i < childCount; i++) {
          final View child = layoutManager.getChildAt(i);
          int childStart = helper.getDecoratedStart(child);

          /** if child is more to start than previous closest, set it as closest  **/
          if (childStart < startest) {
              startest = childStart;
              closestChild = child;
          }
      }
      return closestChild;
  }

可以看出findStartView方法就是查itemViewtop距离RecyerView的上边距距离最近的那一个itemView,然后layoutManager.getPosition(mStartMostChildView)知道这个itemView的位置,最后 vectorProvider.computeScrollVectorForPosition判断滚动的方向,最终返回findTargetSnapPosition上一页或者下一页的位置交给snapFromFling方法中的处理惯性滚动。

到此为止PagerSnapHelper的核心原理分析完毕,本质也就是设置RecyclerViewOnFlingListener对象接管RecyclerView自己的fing惯性滚动,然后在滚动结束后调用snapToTargetExistingView进行翻上一页或者下一页。其实SnapHelper还有一个子类LinearSnapHelper,弄懂了SnapHelper的大致工作原理,分析也不难了,在这里偷个懒就不做分析了。不过研究PageSnapHelper到时能学到更多的东西以及对RecyclerView有了更多的了解,算是无心插柳柳成荫吧。如有不当之处,欢迎批评指正,共同切磋学习

发布了257 篇原创文章 · 获赞 484 · 访问量 144万+

猜你喜欢

转载自blog.csdn.net/chunqiuwei/article/details/103257452
今日推荐