想要了解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原理解析(二)这篇博文中解析