通过RecyclerView之SnapHelper原理解析(一) 这篇文章可知只要实现RecyclerView.OnFlingListener
接口,并将该接口的fling方法返回true就可以简单的将RecyclerView
作为ViewPager
来使用,让RecycerView
分页滑动,原理就是根据滚动的距离/recyerView的高度来计算滚动的当前页数。下面就来说说Android
提供的另外一个库用PageSnapHelper
是怎么工作的。
SnapHepler
是什么?该组件本质上仍然就是一个RecyclerView.OnFlingListener
:
public abstract class SnapHelper extends RecyclerView.OnFlingListener
该类是个抽象类,有两个实现类LinearSnapHelper
和PagerSnapHelper
!关于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
其次通过linearLayoutManager
的getPosition
(view)方法知道当前view的位置,也就是currentPage
的index
。
来看看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方法)!现在就来看看具体的滚动逻辑,
从其父类SnapHelper
,SnapHelper
内置了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;
}
可以看出对于PageSnapHelper
的snapToTargetExistingView
就是找到snapView
的中心距离RecylerView
的中心的距离distanceToCenter
,然后调用smoothScrollBy
滚动即可。对于父类SnapHelper来说snapToTargetExistingView
的用意就是找到snapView
,然后计算出snapView
到达目标位置的距离然后滚动之,使得snapView
达到目标位置,而这个snapView
就是指定要滚动到目标位置的那个View(当然在这里是距离目标位置最近的view)
以上说的都是在RecyclerView
滚动结束后调用snapToTargetExistingView
方法,使得snapView与目标位置对齐达到翻页的效果。
PagerSnapHelper的移动规则是每次滑动将距离中心位置最近的item移动到RecyclerView中心位置
但是文章开头就说过SnapHelper
本质就是一个OnFlingListener
接口,所以之所以能达到翻页的滚动效果还是因为OnFlingListener
接口接管了RecyclerView
的fling
惯性滚动效果,这是前提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
开始滚动的目标位置。 需要注意的是findTargetSnapPosition
在SnapHelper
是一个抽象方法,所以我们来看看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
方法就是查itemView
的top
距离RecyerView
的上边距距离最近的那一个itemView
,然后layoutManager.getPosition(mStartMostChildView)
知道这个itemView
的位置,最后 vectorProvider.computeScrollVectorForPosition
判断滚动的方向,最终返回findTargetSnapPosition
上一页或者下一页的位置交给snapFromFling
方法中的处理惯性滚动。
到此为止PagerSnapHelper
的核心原理分析完毕,本质也就是设置RecyclerView
的OnFlingListener
对象接管RecyclerView
自己的fing惯性滚动,然后在滚动结束后调用snapToTargetExistingView
进行翻上一页或者下一页。其实SnapHelper
还有一个子类LinearSnapHelper
,弄懂了SnapHelper
的大致工作原理,分析也不难了,在这里偷个懒就不做分析了。不过研究PageSnapHelper
到时能学到更多的东西以及对RecyclerView
有了更多的了解,算是无心插柳柳成荫吧。如有不当之处,欢迎批评指正,共同切磋学习