RecyclerView之SnapHelper原理解析(一)

想要了解SnapHelper的工作原理,就要知道Android View的滚动原理和RecyclerView的滚动原理,刚好博主对这方面写了详细的博客,在阅读本篇博文之前,建议对于View的滚动原理尚不熟悉的猿人们读一下博主的下面几篇博客,算是知识储备,当然不读也基本不影响本片博文的阅读:
View的滚动原理简单解析
View的滚动原理简单解析(二)
ViewDragHelper的简单分析(一)
ViewDragHelper的简单分析及应用(二)
RecyclerView的滚动原理

通过《RecyclerView的滚动原理》分析可以知道,RecyclerView的滚动有三种状态:
SCROLL_STATE_IDLE:RecyclerView不再滚动或者停止滚动的状态,当RecyclerView不在滚动或者惯性滚动结束后的状态
SCROLL_STATE_DRAGGING:RecyclerView随着手指拖动而滚动的状态
SCROLL_STATE_SETTLING:RecyclerView随着手指的离开而发生惯性滚动状态,也即是fling滚动状态。

(所谓惯性滚动是指手指离开屏幕后,RecyclerView持续滚动直到停止的滚动,就像骑自行车,双脚离开脚踏后自行车仍然可以因为惯性而保持行驶状态一样)但是惯性滚动有一个特点:滚动结束后,停止到什么位置是不确定的,具有随机性!对于一些场景来说是不合适的,比如用过抖音的都知道,是需要让RecyclerView实现一页一页的滚动!那么类似抖音的滚动功能是怎么实现的呢?其原理是如何?下面就来掰扯掰扯。

最核心的思路就是打破RecyclerView的默认惯性滚动,让我们自己来处理惯性滚动!但是怎么打破默认滚动呢?,下面来分析分析。手指离开屏幕是惯性滚动的开始,所以看看RecyerView onTouchEvent的ACTION_UP事件都做了什么:

 case MotionEvent.ACTION_UP: {
          //省略部分代码
          if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
              setScrollState(SCROLL_STATE_IDLE);
          }
      } break; 

代码整体逻辑很简单,就是在手指离开屏幕后调用fling方法执行惯性滚动,惯性滚动结束后,设置滚动状态为SCROLL_STATE_IDLE,并通知客户端滚动结束。再来看看fing方法,删除了大量与本文无关的代码之后,可以看出端倪:

   public boolean fling(int velocityX, int velocityY) {

   if (!dispatchNestedPreFling(velocityX, velocityY)) {

   //如果设置了mOnFlingListener并且其onFling方法返回true,则让客户端自己执行惯性滚动
   if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
   return true;
   }

   //让RecyclerView自己执行惯性滚动
   mViewFlinger.fling(velocityX, velocityY);
   return true;
   }
   return false;
   }

通过fling方法可以看出RecyclerView是优先执行客户端自己的惯性逻辑的,也即是mOnFlingListener.onFling。如果onFling方法返回发true的话则不会执行RecyclerView自己的惯性滚动,也就是不会调用 mViewFlinger.fling方法, 所以想要实现类似抖音的页面效果,思路就来了,最核心代码就是如下:

 recyclerView.setOnFlingListener(new RecyclerView.OnFlingListener() {
  @Override
	  public boolean onFling(int velocityX, int velocityY) {
	  	return true;
	  }
  });

  recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
	  @Override
	  public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
		  if(newState == RecyclerView.SCROLL_STATE_IDLE){
		  //   //RecyclerView主动调用这个方法之后,这个方法返回true之后会立即调用OnScrollStateChangedListener的
		  //   //onScrollStateChanged方法,此时newState为 SCROLL_STATE_IDLE
		  }
 	  }

	  @Override
	  public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
	  }
  });

所以分页加载的思路就很简单:假设RecyclerView的高度为h,且item的高度也是h,那么滚动的距离mTotalScrolly/h就等于当前页数。代码如下:

  /**
     * 获取当前为第几页
     * @return
     */
    private int getCurrentPageIndex() {
        return mStartScrollY / mRecyclerView.getHeight();
    }
    /**
     * 获取即将出来的最新页
     * @return
     */
    private int getNewPageIndex() {
        return mTotalScrollY / mRecyclerView.getHeight();
    }

其中mStartScrollY是每次开始滚动的距离,或者说是记录了上次RecyclerView滚动的距离;而mTotalScrolly为RecyclerView每次滚动的总距离。二者的关系用下面代码来解释:

  private class OnScrollStateChangedListener extends RecyclerView.OnScrollListener {
       
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if(dy!=0){
               //累加滚动的总距离
                mTotalScrollY += dy;
            }
        }
    }

//如果滚动结束
if(finishScroll){
   mStartScrollY = mTotalScrollY;
}

上面说到每次手指抬起的时候,我们自己处理fling,那么fling的代码逻辑就如下所示了:

  public boolean onFling(int velocityX, int velocityY) {
      //获取开始滚动时所在页面的index
        int currentPage = getCurrentPageIndex();
        if (velocityY < 0) {//上一页
            currentPage--;
        } else if (velocityY > 0) {//下一页
            currentPage++;
        }
        int endY = currentPage * mRecyclerView.getHeight();
        if (endY < 0) {
            endY = 0;
        }
        //剩下的距离
        int scrollDistance = endY-mTotalScrollY;
        scroll(mTotalScrollY, scrollDistance);
  }
      

这里的scroll方法简单了利用了Scroller这个组件:

   private void autoScroll() {
        Message msg = Message.obtain();
        mHandler.sendMessage(msg);
    }
    private Scroller scroller;
    private void scroll(int startY, int dy) {
        scroller.forceFinished(true);
        scroller = new Scroller(mRecyclerView.getContext());
        scroller.startScroll(0, startY, 0, dy, 300);
        autoScroll();
    }
    private Handler mHandler = new Handler() {
        int mCurrentPage=0;
        public void handleMessage(Message msg) {
            if (scroller.computeScrollOffset()) {//滚动尚未结束
                //获取已经滚动的位置
                int currentY = scroller.getCurrY();
                mRecyclerView.scrollBy(0, currentY - mTotalScrollY);
                autoScroll();//继续滚动
            } else {
              
                //主要是设置mRecyclerView的滚动状态为SCROLL_STATE_IDLE
                mRecyclerView.stopScroll();
                mStartScrollY = mTotalScrollY;
            }
        }
    };

另外还有一个需要注意的地方,就是在滚动结束后,如果一个item正好滚动超过了屏幕的一半,那么就要自动滚动上一个或者下一个页面,这个需要在onScrollStateChanged方法里面处理:

  @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
         
            int diff = mTotalScrollY - mStartScrollY;
            //如果滑动的距离超过屏幕的一半表示需要滑动到下一页
            boolean move = Math.abs(diff) > recyclerView.getHeight() / 2;
            int velocityY = 0;
            if (move) {
                velocityY = diff < 0 ? -1 : 1;

            }
            fing(0,velocityY);
        }

到此为止,分页加载的大致思路讲解完毕,完整的demo代码点此可得。此demo只供学习使用,此demo主要是为了了解滚动的原理以及后面SnapHepler源码分析做了铺垫,不能用于实际开发使用!另外关于分页加载的,Google自己提供了一套PageSnapHelper,这套组件的根本原理也就是博主说的自己处理惯性滚动,也即是实现了自己的RecyclerView.OnFlingListener,而不是让RecycerView自己处理惯性滚动。关于该组件的详细细节,后面会在RecyclerView之SnapHelper原理解析(二)这篇博文中解析

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

猜你喜欢

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