简介
开发中经常会遇到需要做一个页菜单效果,类似淘宝、京东之类的app的左右滑动分页菜单效果,这种效果虽然可以用ViewPager去做但是总觉得不够优雅。今天来介绍一个使用RecyclerView的实现方式,这种方式会好用很多,因为横向的功能列表往往是根据配置来的,可能还有排序。
效果图(1)如下:
问题分析
1.RecyclerView并没有提供类似的布局的LayoutManager,这里我们需要一个排列效果为“每8个item为一个Page,每个page分为2行4列”,排列应该是下面这样的,这里我们需要自定义LayoutManager。效果图(2)
2.RecyclerView提供了辅助类SnapHelper,用于辅助RecyclerView在滚动结束时将Item对齐到某个位置。SnapHelper是一个抽象类,官方提供了LinearSnapHelper和PagerSnapHelper两个子类,但是这2个辅助类都是拿屏幕中间的item进行居中对齐,显然这2个辅助类我们也用不了,所以这里需要自定义SnapHolper来实现ViewPager翻页的效果。
拿来主义者
如果你不想了解实现过程,只想拿来使用,https://github.com/844646669/ViewPageRecyclerView 这个GitHub地址,下载下来后,直接coyp CustomLayoutManager和CustomSnapHelper这两个类,使用方法如下:
recyclerView.setLayoutManager(new CustomLayoutManager(2,4));
recyclerView.setAdapter(new Adapter2());
new CustomSnapHelper().attachToRecyclerView(recyclerView);
自定义LayoutManager实现特殊的布局效果
创建一个CustomLayoutManager类,继承RecyclerView.LayoutManager,会强制重写generateDefaultLayoutParams方法,这个方法是给RecyclerView子View添加默认LayoutParams的,如果RecyclerView.Adapter的onCreateViewHolder方法中创建的view没有LayoutParams,那么添加到RecyclerView中就会调用这个方法为其添加默认的LayoutParams对象。
public class CustomLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return null;
}
}
一般来说,没有什么特殊需求的话,可以直接让子item自己决定自己的宽高
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
}
到这里,这个CustomLayoutManager已经可以使用了,只是运行后会发现界面是空白的,这是因为我们没有布局任何item。所以我们需要再重写onLayoutChildren方法。这个方法就是用于给每个item布局的,讲大白话就是为RecyclerView添加Item并控制每个item的位置,这个方法有2个参数,recycler参数是用于View的回收和再利用的,state参数是用于判断状态的,这里我们不需要用到。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
}
在学习onLayoutChildren之前,我们需要先了解一下几个常用的方法:
1、getItemCount()
这个是RecyclerView.LayoutManager自带的一个方法,用与获取当前列表总共有多少个Item,该方法最终调用Adapter中的getItemCount方法获取item个数。
2、getViewForPosition(int position)
这个是recycler的一个方法,可以根据索引获取对应的Item。
3、measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed)
这个是RecyclerView.LayoutManager自带的一个方法,用于测量子Item(child)的,需要注意的是后面的widthUsed和heightUsed,这2个参数是宽度和高度的使用量不是剩余量,这里很容易忽略。
4、getDecoratedMeasuredWidth(@NonNull View child) 和 getDecoratedMeasuredHeight(@NonNull View child)
这个是RecyclerView.LayoutManager自带的一个方法,在measureChildWithMargins方法测量之后获取Item测量的结果
5、layoutDecorated(@NonNull View child, int left, int top, int right, int bottom)
这个是RecyclerView.LayoutManager自带的一个方法,用于将Item布局到对应的位置
常用方法看完了,回到上面onLayoutChildren中,这里我们需要将子Item按照 ”效果图(2)“中那样把每个item添加到RecyclerView中去:
int line = 2; //每一页line行
int col = 4; //每一行 col列
//每个item对应的位置信息
private List<Rect> rectFList = new ArrayList<>();
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
int itemCount = getItemCount(); //获取item数量
if (itemCount <= 0) {
return;
}
View fistView = recycler.getViewForPosition(0);//获取第一个item
int width = getWidth();//获取RecyclerView的宽度
//这里考虑到RecyclerView可能有内边距,所以实际可用宽度应该是减去左右内边距
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
/**
* measureChildWithMargins方法的后两个参数是宽度和高度的使用量,
* 这里每一行是有4列,所以宽度的使用量应该是3/4 canUseWidth
*/
int usedWidth = Math.round(canUseWidth * ((col - 1f)/col));
int usedHeight = 0;//高度不受限制
//测量第一个Item,这里只测量第一个item,是默认所有的Item大小都是一样的
measureChildWithMargins(fistView,usedWidth, usedHeight);
//获取测量后Item的宽高
int measureWidth = getDecoratedMeasuredWidth(fistView);
int measureHeight = getDecoratedMeasuredHeight(fistView);
int colWidth = Math.round((canUseWidth + 0f) / col); //计算出每一列的宽度
//计算所有Item的位置情况
rectFList.clear();
for (int i = 0; i < itemCount; i++) {
int tarPage = getTarPage(i); //获取第i个Item对应的页码(1、2、3)
int tarLine = getTarLine(i); //获取对应的行码 (0、1、2、3)
int tarCol = getTarCol(i); //获取对应的列码(0、1、2、3)
//计算第tarPage页的偏移量 (每页宽度 * 页码) + 左边内边距
int xOffset = canUseWidth * (tarPage - 1) + getPaddingLeft();
//计算 第 i个Item的 上下左右位置
int left = colWidth * tarCol + xOffset;
int top = measureHeight * tarLine;
int right = left + colWidth;
int button = top + measureHeight;
Rect rect = new Rect(left, top,right, button);
rectFList.add(rect);
}
//将所有的Item添加到RecyclerView中并设置对应的位置
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
View view = recycler.getViewForPosition(i);
measureChildWithMargins(view,usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//设置对应Item在RecyclerView中的位置
layoutDecorated(view,rect.left,rect.top,rect.left + viewWidth, rect.top + viewHeight);
}
}
/**
* 获取第position个Item在其所在Page对应的行数(0、1、... (line -1))
* @param position
* @return
*/
private int getTarLine(int position) {
int line = (int) Math.ceil((position + 1f) / this.col);
line = (line - 1)%this.line;
return (int) line;
}
/**
* 获取第position个Item在其所在Page对应的列数(0、1、... (col -1))
* @param position
* @return
*/
private int getTarCol(int position) {
int col = (int) Math.ceil((position) % this.col);
return col;
}
/**
* 获取第position个Item在所在的page(1、2、3、4.。。)
* @param position
* @return
*/
public int getTarPage(int position) {
int onePageCount = line * col;
return (int) Math.ceil((position + 1f) / onePageCount);
}
/**
* 这个返回true可以自动根据所有Item的宽高,重新测试RecyclerView的宽高
* @return
*/
@Override
public boolean isAutoMeasureEnabled() {
return true;
}
到这里,RecyclerView已经按我们的要求把每个Item都排列好了,效果如下,但是还不能左右滑动。
需要给CustomLayoutManager添加左右滑动支持。需要重写canScrollHorizontally 和 scrollHorizontallyBy方法.
1、boolean canScrollHorizontally() 和 boolean canScrollVertically()
这2个方法返回横向和纵向是否支持滑动,返回true则支持,false不支持
2、scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)和scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
这2个方法就是用来控制滑动逻辑,第一个参数就是滑动距离
这里我们的控件只需要支持左右滑动,所以只需要重写canScrollHorizontally 和 scrollHorizontallyBy方法。
canScrollHorizontally方法很简单,只需要返回true就可以了
@Override
public boolean canScrollHorizontally() {
return true;
}
scrollHorizontallyBy也不难,第一个参数是本次滑动距离,我们只需要根据做大滑动值和最小滑动值计算出本次真实的滑动值,并调用offsetChildrenHorizontal方法执行滑动就可以了。
int mXOffset = 0;
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
int itemCount = getItemCount(); //获取item数量
if (itemCount == 0) return 0;
//获取最后一个item对应的page值就等于获取到总的page数量
int pageCount = getTarPage(itemCount - 1);
int width = getWidth();//获取RecyclerView的宽度
//这里考虑到RecyclerView可能有内边距,所以实际可用宽度应该是减去左右内边距
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
int minOffset = 0;//最小滑动值是0
//每页宽度 * (页面数量 - 1) 得到最大滑动距离 (只有1页的时候是不能滑动的)
int maxOffset = canUseWidth * (pageCount - 1);
int realDx = dx; //真实滑动距离
if (mXOffset + dx < minOffset) { //如果滑动距离小于最小值,则修正真实滑动距离
realDx = 0 - mXOffset;
}
if (mXOffset + dx > maxOffset) { //如果滑动距离小于最大值,则修正真实滑动距离
realDx = maxOffset - mXOffset;
}
//执行滑动操作
offsetChildrenHorizontal(-realDx);
//计算新的mXOffset
mXOffset = mXOffset + realDx;
//返回值是本次滑动的真实距离,RecyclerView是支持嵌套滚动的,这里涉及到嵌套滚动的内容就不做赘述了
return realDx;
}
到这里,我们的列表就可以正常滑动了,但是这样是有缺陷的,就是我们一开始就把所有的Item都添加到RecyclerView中去,而同一时间界面上只会显示其中的部分Item,大部分Item都是不可见的,如果Item数量特别多的时候就会创建很多个Item导致不必要的内存占用如如果Item可以回收和再利用的话,每次只添加一小部分的Iitem到RecyclerView中去,这样可以达到性能优化的目的。
RecyclerView的回收复用
其实RecyclerView内部已经为我们实现了回收复用所需要的所有条件,但需要在LayoutManager中控制控制每个Item是否可以继续使用或者是把它放到回收池中去。同时获取Item时也可以先去回收池中查找是否已经创建了对应类型的Item,如果有,则可以直接使用不需要再重新创建。很明显,我们上面的代码中,我们一开始就把所有的Item都布局到RecyclerView中去,滑动时也只是简单的进行内容滑动,并没有对移出屏幕的Item做回收处理。更别说对Item的复用了。
RecyclerView的回收池
RecyclerView默认提供了mCachedViews和mRecyclerPool两级缓存,用来保存这些需要回收的Item,这两个缓存的区别是:mCachedViews是一级缓存,它的size是2,只能保存两个Item,这里保存的始终是最新被移除的Item,当mCachedViews满了之后,按照先进先出的原则把老的Item存放到mRecyclerPool中去,mRecyclerPool默认size是5.这就是RecyclerVeiw默认提供的二级缓存,除此之外,还预留了mViewCacheExtension一个扩展回收池,不过一般不会用到。使用removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler)
方法可以将Item从RecyclerView界面面上剥离下来并存放到回收池中,具体存放在哪个回收池按照上面规则用又recycler对象自行处理。
Detach和Scrap
除了回收池,还有另一种场景,就是当列表数据发生变动时(例如调用Adapter的notifyDataSetChanged()
方法),会回调LayoutManager的onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
方法,我们需要对列表进行重新布局,这种情况下需要对先对当前界面上的所有Item剥离下来再按照新的顺序布局到列表上。这个时候需要用到detachAndScrapAttachedViews(@NonNull Recycler recycler)
方法,它的作用是将当前界面上的所有Item全部剥离下来,然后保存在一个叫mAttachedScrap的列表中,以供重新布局的时候使用,所以在自定义的LayoutManager的onLayoutChildren方法中,经常可以看到第一件事就是先调用detachAndScrapAttachedViews方法,原因就在这里。
到这里,我们总共有mAttachedScrap、mCachedViews、mRecyclerPool、mViewCacheExtension 4个可以存放Item的地方,其中mAttachedScrap比较特殊,需要调用detachAndScrapAttachedViews
方法才能添加进去,其他3个调用removeAndRecycleView
方法可以添加进去。
Item复用
上面讲到4个保存Item的地方,那么要怎么从这些缓存中取出Item进行复用呢?我们一般使用recycler.getViewForPosition(int position)
方法获取,它会先在mAttachedScrap列表中查找,看看有没有刚刚被剥离的Item,如果有就直接返回,如果没有,那就去mCachedViews缓存中查找,如果有就直接返回,如果还是没有,那就去mRecyclerPool中查找,如果有就直接返回,如果没有,那就去mViewCacheExtension 中查找,如果都没有,最后才会去调用Adapter的onCreateViewHolder方法新建一个。需要注意的是mAttachedScrap和mCachedViews中的Item都是精准匹配,所以都是直接使用,不会调用Adapter的onBindViewHolder重新绑定数据,mRecyclerPool和mViewCacheExtension 都是根据ViewType来匹配的,所以会调用Adapter的onBindViewHolder重新绑定数据。
实现支持Item回收复用的LayoutManager
回到我们的CustomLayoutManager中来,上边的onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
和scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)
方法都需要修改。
先看onLayoutChildren方法,我们需要先将界面上所有的Item都剥离下来
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler); //把Item从界面上剥离下来
...(之前的内容)
}
如图所示,根据当前偏移量,计算当前屏幕左右两边对应的X坐标值:
pageStartX = mXOffset + paddingLeft
pageEndX = pageStartX + canUseWidth
这里需要缓存屏幕左右两边各一个屏幕大小的缓冲区(后面实现ViewPager吸附效果需要用到),所以实际上需要添加Item的区域应该是:pageStartX - canUserWidth 到 pageEndX + canUserWidth 这个区域。
//计算当前界面上显示对应的区域
int pageStartX = mXOffset + getPaddingLeft();
int pageEndX = pageStartX + canUseWidth;
//缓存当前界面相邻一个page范围
pageStartX = pageStartX - canUseWidth;
pageEndX = pageEndX + canUseWidth;
遍历所有item,判断该item是否在区域内,如果在,则添加到界面上去。
//将所有的Item添加到RecyclerView中并设置对应的位置
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
//判断Item是否处于pageStartX到pageEndX区间内,如果是就把对应的Item添加到RecyclerView中去
if (rect.right > pageStartX && rect.left < pageEndX) {
View view = recycler.getViewForPosition(i); //回收池中获取Item
measureChildWithMargins(view, usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//这里位置是相对于屏幕的位置,所以需要减去偏移量才是当前Item的位置
int left = rect.left - mXOffset - getPaddingLeft();
int top = rect.top;
int right = left + viewWidth;
int button = top + viewHeight;
Log.d("ccm" + i,String.format("left %d, right %d", left,right));
layoutDecorated(view, left, top, right, button);
}
}
所以onLayoutChildren方法的最终代码应该是这样的:
int line = 2; //每一页line行
int col = 4; //每一行 col列
//每个item对应的位置信息
private List<Rect> rectFList = new ArrayList<>();
int mXOffset = 0; //当前滑动偏移量(scrollHorizontallyBy方法中记录的偏移量)
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler); //把Item从界面上剥离下来
int itemCount = getItemCount(); //获取item数量
if (itemCount <= 0) {
return;
}
View fistView = recycler.getViewForPosition(0);//获取第一个item
int width = getWidth();//获取RecyclerView的宽度
//这里考虑到RecyclerView可能有内边距,所以实际可用宽度应该是减去左右内边距
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
/**
* measureChildWithMargins方法的后两个参数是宽度和高度的使用量,
* 这里每一行是有4列,所以宽度的使用量应该是3/4 canUseWidth
*/
int usedWidth = Math.round(canUseWidth * ((col - 1f)/col));
int usedHeight = 0;//高度不受限制
//测量第一个Item,这里只测量第一个item,是默认所有的Item大小都是一样的
measureChildWithMargins(fistView,usedWidth, usedHeight);
//获取测量后Item的宽高
int measureWidth = getDecoratedMeasuredWidth(fistView);
int measureHeight = getDecoratedMeasuredHeight(fistView);
int colWidth = Math.round((canUseWidth + 0f) / col); //计算出每一列的宽度
//计算所有Item的位置情况
rectFList.clear();
for (int i = 0; i < itemCount; i++) {
int tarPage = getTarPage(i); //获取第i个Item对应的页码(1、2、3)
int tarLine = getTarLine(i); //获取对应的行码 (0、1、2、3)
int tarCol = getTarCol(i); //获取对应的列码(0、1、2、3)
//计算第tarPage页的偏移量 (每页宽度 * 页码) + 左边内边距
int xOffset = canUseWidth * (tarPage - 1) + getPaddingLeft();
//计算 第 i个Item的 上下左右位置
int left = colWidth * tarCol + xOffset;
int top = measureHeight * tarLine;
int right = left + colWidth;
int button = top + measureHeight;
Rect rect = new Rect(left, top,right, button);
rectFList.add(rect);
}
//计算当前界面上显示对应的区域
int pageStartX = mXOffset + getPaddingLeft();
int pageEndX = pageStartX + canUseWidth;
//缓存当前界面相邻一个page范围
pageStartX = pageStartX - canUseWidth;
pageEndX = pageEndX + canUseWidth;
//将所有的Item添加到RecyclerView中并设置对应的位置
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
//判断Item是否处于pageStartX到pageEndX区间内,如果是就把对应的Item添加到RecyclerView中去
if (rect.right > pageStartX && rect.left < pageEndX) {
View view = recycler.getViewForPosition(i); //回收池中获取Item
measureChildWithMargins(view, usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//设置对应Item在RecyclerView中的位置
int left = rect.left - mXOffset - getPaddingLeft(); //这里需要减去偏移量才是当前Item的位置
int top = rect.top;
int right = left + viewWidth;
int button = top + viewHeight;
Log.d("ccm" + i,String.format("left %d, right %d", left,right));
layoutDecorated(view, left, top, right, button);
}
}
}
scrollHorizontallyBy
方法也需要修改,同样根据当前偏移量计算出当前屏幕左右对应的x值,然后根据realDx计算出滑动后屏幕左右对应的x值:
//根据偏移量计算当前显示区域
int pageStartX = mXOffset + getPaddingLeft();
int pageEndX = pageStartX + canUseWidth;
//根据realDx计算移动后的显示区域
pageStartX += realDx;
pageEndX += realDx;
//缓存界面相邻一个page范围
pageStartX -= canUseWidth;
pageEndX += canUseWidth;
获取当前界面上所有Item,判断是否不在区域内,如果不在,需要剥离下来并添加到回收池中:
//遍历已添加到RecyclerView上的Item,判断是否处于pageStartX到pageEndX范围内,如果不是
//给他移除掉添加到回收池中去
int childCount = getChildCount();//添加到界面上的Item数量
List<View> needRemoveView = new ArrayList<>(2);
for (int i = 0; i < childCount; i++) {
View item = getChildAt(i);
int position = getPosition(item);
Rect rect = rectFList.get(position);
if (!(rect.right > pageStartX && rect.left < pageEndX)) {
//不处于pageStartX到pageEndX范围内的Item需要移除
needRemoveView.add(item);
}
}
for (View item : needRemoveView) {
removeAndRecycleView(item, recycler); //移除掉
}
遍历所有Item,判断是否在区域内,如果在区域内,判断是否已经添加到界面上,如果没有,添加到界面上
//添加Item
int usedWidth = Math.round(canUseWidth * ((col - 1f)/col));
int usedHeight = 0;//高度不受限制
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
//判断Item是否处于pageStartX到pageEndX区间内,如果是就把对应的Item添加到RecyclerView中去
if (rect.right > pageStartX && rect.left < pageEndX) {
View view = null;
//查找该Item是否已经在界面上了
view = findViewByPosition(i);
if (view == null) { //界面上查找不到,说明需要添加
view = recycler.getViewForPosition(i); //回收池中获取Item
measureChildWithMargins(view, usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//设置对应Item在RecyclerView中的位置
int left = rect.left - mXOffset - getPaddingLeft(); //这里需要减去偏移量才是当前Item的位置
int top = rect.top;
int right = left + viewWidth;
int button = top + viewHeight;
layoutDecorated(view, left, top, right, button);
}
}
}
所有scrollHorizontallyBy
方法的最终代码如下:
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
int itemCount = getItemCount(); //获取item数量
if (itemCount == 0) return 0;
//获取最后一个item对应的page值就等于获取到总的page数量
int pageCount = getTarPage(itemCount - 1);
int width = getWidth();//获取RecyclerView的宽度
//这里考虑到RecyclerView可能有内边距,所以实际可用宽度应该是减去左右内边距
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
int minOffset = 0;//最小滑动值是0
//每页宽度 * (页面数量 - 1) 得到最大滑动距离 (只有1页的时候是不能滑动的)
int maxOffset = canUseWidth * (pageCount - 1);
int realDx = dx; //真实滑动距离
if (mXOffset + dx < minOffset) { //如果滑动距离小于最小值,则修正真实滑动距离
realDx = 0 - mXOffset;
}
if (mXOffset + dx > maxOffset) { //如果滑动距离小于最大值,则修正真实滑动距离
realDx = maxOffset - mXOffset;
}
//根据偏移量计算当前显示区域
int pageStartX = mXOffset + getPaddingLeft();
int pageEndX = pageStartX + canUseWidth;
//根据realDx计算移动后的显示区域
pageStartX += realDx;
pageEndX += realDx;
//缓存界面相邻一个page范围
pageStartX -= canUseWidth;
pageEndX += canUseWidth;
//遍历已添加到RecyclerView上的Item,判断是否处于pageStartX到pageEndX范围内,如果不是
//给他移除掉添加到回收池中去
int childCount = getChildCount();//添加到界面上的Item数量
List<View> needRemoveView = new ArrayList<>(2);
for (int i = 0; i < childCount; i++) {
View item = getChildAt(i);
int position = getPosition(item);
Rect rect = rectFList.get(position);
if (!(rect.right > pageStartX && rect.left < pageEndX)) {
//不处于pageStartX到pageEndX范围内的Item需要移除
needRemoveView.add(item);
}
}
for (View item : needRemoveView) {
removeAndRecycleView(item, recycler); //移除掉
}
//添加Item
int usedWidth = Math.round(canUseWidth * ((col - 1f)/col));
int usedHeight = 0;//高度不受限制
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
//判断Item是否处于pageStartX到pageEndX区间内,如果是就把对应的Item添加到RecyclerView中去
if (rect.right > pageStartX && rect.left < pageEndX) {
View view = null;
//查找该Item是否已经在界面上了
view = findViewByPosition(i);
if (view == null) { //界面上查找不到,说明需要添加
view = recycler.getViewForPosition(i); //回收池中获取Item
measureChildWithMargins(view, usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//设置对应Item在RecyclerView中的位置
int left = rect.left - mXOffset - getPaddingLeft(); //这里需要减去偏移量才是当前Item的位置
int top = rect.top;
int right = left + viewWidth;
int button = top + viewHeight;
layoutDecorated(view, left, top, right, button);
}
}
}
//计算新的mXOffset
mXOffset = mXOffset + realDx;
//执行滑动操作
offsetChildrenHorizontal(-realDx);
//返回值是本次滑动的真实距离,RecyclerView是支持嵌套滚动的,这里涉及到嵌套滚动的内容就不做赘述了
return realDx;
}
到这里,我们的CustomLayoutManager基本上就写完了,效果如下:
自定义SnapHelper实现ViewPage翻页效果
SnapHelper是RecyclerView的一个辅助类,用于辅助RecyclerView控制滚动结束时将Item进行对齐。继承SnapHelper类需要强制重写3个方法:
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY)
这个方法会在触发Fling时被调用(Fling是指:手指离开屏幕后的惯性滚动),需要根据速度计算出需要滚动到具体的Item,并将该Item对应的position返回。
public View findSnapView(RecyclerView.LayoutManager layoutManager)
这个方法会在SnapHelper关联RecyclerView和Fling结束后被调用,需要根据LayoutManager当前的情况,计算出需要对齐的Item,并返回。
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView)
当findSnapView方法返回值不为null时,这个方法会被调用,需要根据参数targetView计算出当前界面到对齐targetView需要滚动的距离,返回值是一个长度为2的数组,其中int[0]代表需要滚动的X轴距离,int[1]代表需要滚动的Y轴距离。
SnapHelper使用
SnapHelper使用很简单,只需要通过attachToRecyclerView方法关联RecyclerView即可
new CustomSnapHelper().attachToRecyclerView(recyclerView);
SnapHelper源码解析
先来看看关联方法attachToRecyclerView(recyclerView)
:
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks(); //移除之前的所有回调
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();//设置新的回调
//创建Scroller
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
//这个方法会进行一个对齐
snapToTargetExistingView();
}
}
可以看到attachToRecyclerView(@Nullable RecyclerView recyclerView)
方法会先移除掉所有的回调,然后添加新的回调,同时进行一次Item对齐。
先看一下snapToTargetExistingView()
方法是怎么进行对齐的:
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager); //通过findSnapView方法找到对齐的Item
if (snapView == null) {
return;
}
//通过calculateDistanceToFinalSnap方法计算对齐需要滚动距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
//调用recyclerView的smoothScrollBy方法实现滚动
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
snapToTargetExistingView()
方法非常简单,先是通过调用findSnapView(layoutManager)
方法找到需要对齐的Item,如果找到,再调用calculateDistanceToFinalSnap(layoutManager, snapView)
方法计算出需要滚动的距离,然后调用recyclerView的smoothScrollBy()
方法实现对齐。这也就是上面说到findSnapView(layoutManager)
方法会在关联RecyclerView的时候调用一次的原因,同时当findSnapView(layoutManager)
方法返回不为null时calculateDistanceToFinalSnap(layoutManager, snapView)
方法会被调用。
接下来看看setupCallbacks()
和destroyCallbacks()
方法,设置和删除回调:
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
可以看到这里给recyclerView添加了OnScrollListener和OnFlingListner两个回调。其中mScrollListener是一个匿名内部类,代码如下:
private final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//newState == RecyclerView.SCROLL_STATE_IDLE表示RecyclerView已经停止滚动
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
mScrollListener通过mScrolled判断RecyclerView之前是否滚动过,并通过newState == RecyclerView.SCROLL_STATE_IDLE判断当前RecyclerView是否时停滚动状态,也就是说,当RecyclerView滚动到停下来的时候触发,调用snapToTargetExistingView()
方法进行一次对齐,也就是说mScrollListener的作用就是在停止滚动的时候进行一次对齐,由此我们可以得到第二个结论:每次滚动结束,我们写的SnapHelper中findSnapView(RecyclerView.LayoutManager layoutManager)
和calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView)
方法会被调用。
再来看看OnFlingListener回调,这个回调对象时SnapHelper自己mRecyclerView.setOnFlingListener(this);
,直接看回调方法onFling(int velocityX, int velocityY
):
@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;
}
int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); //获取触发Filing的最下速度
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);//调用snapFromFling
}
调用了snapFromFling()
方法:
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判断layoutManager是否实现了ScrollVectorProvider接口
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
//创建SmoothScroller对象
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
//调用findTargetSnapPosition查找需要滚动到的目标Posiont
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
//将targetPosition设置到smoothScroller中
smoothScroller.setTargetPosition(targetPosition);
//开始滚动
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
snapFromFling方法做了4件事,1:判断layoutManager是否实现了ScrollVectorProvider;2:创建SmoothScroller对象;3:调用findTargetSnapPosition返回查找到本次Fling需要滚动到第几个Item;4:调用layoutManager.startSmoothScroll()方法开始滚动。而layoutManager.startSmoothScroll()方法最终也是调用smoothScroller.start()方法把滚动交给SmoothScroller对象完成。
到这里,我们已经知道SnapHelper的大概原理了,总结一下就是:SnapHelper在关联RecyclerView的时候会进行一次对齐操作,同时通过添加OnScrollListener和OnFlingListener两个监听实现辅助对齐的,其中OnScrollListener用于在滚动结束后进行辅助对齐,OnFlingListner在触发FLing时辅助惯性滑动,具体的弹性滚动则由SmoothScroller类处理,同时要实现Fling辅助控制LayoutManager必须实现ScrollVectorProvider接口
讲到这里,我们只剩下最后几个问题:
1.LayoutManager为什么要实现ScrollVectorProvider接口,ScrollVectorProvider是干什么用的?
2.前边写CustomLayoutManager的时候提到,要缓存当前界面左右一个屏幕大小的Item,为什么要这么做?
3.翻页时如何控制翻页速度?
这几个问题都涉及到滚动的具体细节,由于篇幅问题,我这里只讲关键代码,如果想看具体源码分析,可以给我留言,我找个时间写一下。前边提到开始Fling滚动是通过调用layoutManager.startSmoothScroll(smoothScroller)
实现的,这个方法会调用mSmoothScroller.start(mRecyclerView, this);
在start方法中会获取需要滚动到的ItemmTargetView = findViewByPosition(getTargetPosition())
void start(RecyclerView recyclerView, LayoutManager layoutManager) {
...其他内容
mRunning = true;
mPendingInitialRun = true;
//就是这句,getTargetPosition()就是前边findTargetSnapPosition()返回的那个值
mTargetView = findViewByPosition(getTargetPosition());
mRecyclerView.mViewFlinger.postOnAnimation();
mStarted = true;
}
下面的mRecyclerView.mViewFlinger.postOnAnimation()
是一个异步操作,最终会回调到SmoothScroller.onAnimation(int dx, int dy)
方法:
void onAnimation(int dx, int dy) {
...其他内容
//当mTargetView == null时会进入到这里
if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) {
//这个方法中会判断layoutManager是否实现了ScrollVectorProvider接口,并调用computeScrollVectorForPosition方法
PointF pointF = computeScrollVectorForPosition(mTargetPosition);
if (pointF != null && (pointF.x != 0 || pointF.y != 0)) {
recyclerView.scrollStep(
(int) Math.signum(pointF.x),
(int) Math.signum(pointF.y),
null);
}
}
mPendingInitialRun = false;
if (mTargetView != null) {
// verify target position
if (getChildPosition(mTargetView) == mTargetPosition) {
//如果mTargetView不为null,调用onTargetFound方法
onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
mRecyclingAction.runIfNecessary(recyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
mTargetView = null;
}
}
...其他内容
}
现在可以回答第一个问题了,ScrollVectorProvider接口是当mTargetView为null时用的,如果返回的pointF.x等于0则不需要滚动,如果大于0则向友滚动,小于0则向左滚动。并且这个滚动时匀速的。直到mTargetView不为null。所以我们的CustomLayoutManager如果要支持Filing那就必须实现ScrollVectorProvider接口,需要实现方法public PointF computeScrollVectorForPosition(int targetPosition)
@Nullable
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
Rect rect = rectFList.get(targetPosition);
int offset = rect.left - mXOffset;
PointF pointF = new PointF(offset, 0);
return pointF;
}
第二个问题,LayoutManager中为啥要缓存当前界面左右各一个屏幕距离的Item?前面提到mTargetView是通过findViewByPosition()
方法获取的,这个方法只会获取到已经添加到界面中的Item,如果没获取到,那么列表将已匀速滚动,直到对应Item被添加到界面中才调用onTargetFound()
改为弹性滚动。我们要做的是ViewPager的翻页效果,显然不想匀速滚动,所以我这里选择了缓冲屏幕左右两边一个屏幕大小的Item。
第三各问题,翻页时如何控制翻页速度?前面代码中snapFromFling方法中是通过调用createScroller(layoutManager)
方法获取RecyclerView.SmoothScroller对象的,这里我们只需要重写这方法即可:
@Nullable
@Override
protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
return !(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider) ? null : new LinearSmoothScroller(mRecyclerView.getContext()) {
//还记得这个方法吗,前边onAnimation方法中,如果mTargetView不为null,调用的就是这个方法
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
//计算滚动到targetView需要滚动的距离
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),targetView);
int dx = snapDistances[0];
int dy = snapDistances[1];
int time = this.calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
//开始弹性滚动
action.update(dx, dy, time, this.mDecelerateInterpolator);
}
}
//这个方法返回的是每Px距离需要滑动的时间
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 100.0F / (float)displayMetrics.densityDpi;
}
//这个返回的是dx距离需要滚动的时间
protected int calculateTimeForScrolling(int dx) {
//我们是viewPager效果,翻页速度要比较块,这里加了个现在,最大时间是100毫秒
return Math.min(100, super.calculateTimeForScrolling(dx));
}
};
}
接下来我们可以开始写我们的CustomSnapHelper类了。(这里需要为CustomLayoutManager添加几个辅助方法供CustomSnapHelper用),这里我直接把思路注释在代码里面了_
public class CustomSnapHelper extends SnapHelper {
private RecyclerView mRecyclerView;
@Override
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
this.mRecyclerView = recyclerView;
super.attachToRecyclerView(recyclerView);
}
/**
* 计算滚动到targetView需要滚动的距离
* @param layoutManager
* @param targetView
* @return
*/
@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
Log.d("ccm", "calculateDistanceToFinalSnap");
if (layoutManager instanceof CustomLayoutManager) {
CustomLayoutManager customLayoutManager = (CustomLayoutManager) layoutManager;
int position = customLayoutManager.getPosition(targetView); //得到targetView对应的position
PointF pointF = customLayoutManager.computeScrollForPosition(position); //计算需要滚动距离
int[] a = new int[2];
a[0] = Math.round(pointF.x);
a[1] = Math.round(pointF.y);
return a;
} else {
int[] a = new int[2];
a[0] = 0;
a[1] = 0;
Log.d("ccm", "calculateDistanceToFinalSnap");
return a;
}
}
/**
* 计算出当前页面需要对齐的Item
* @param layoutManager
* @return
*/
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof CustomLayoutManager) {
int childCount = layoutManager.getChildCount();
if (childCount <= 0) return null;
//获取当前屏幕上最小的Item
int minItem = 9999;
for (int i = 0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
int decoratedRight = layoutManager.getDecoratedRight(child);
int decoratedLeft = layoutManager.getDecoratedLeft(child);
if (decoratedRight > layoutManager.getPaddingLeft() && decoratedLeft < layoutManager.getWidth() - layoutManager.getPaddingRight()) {
int position = layoutManager.getPosition(child);
minItem = Math.min(minItem, position);
}
}
if (minItem == 9999) return null;
CustomLayoutManager customLayoutManager = (CustomLayoutManager) layoutManager;
int tarPage = customLayoutManager.getTarPage(minItem);//计算出minItem对应的page
int maxPage = customLayoutManager.getMaxPage(); //获取总的page数量
int pageFistPosition = customLayoutManager.getPageFistPosition(tarPage); //获取当前page的第一个Item
if (tarPage == maxPage) { //如果当前pege已经是最后一页了,返回当前page的第一个Item
return customLayoutManager.findViewByPosition(pageFistPosition);
}
int nextPageFistPosition = customLayoutManager.getPageFistPosition(tarPage + 1); //获取下一个page的第一个Item
//得到当前page的第一个Item和下一个page的第一个Item 对应的位子数据
Rect lastRect = customLayoutManager.getRectByPosition(pageFistPosition);
if (lastRect == null) return null;
Rect thisRect = customLayoutManager.getRectByPosition(nextPageFistPosition);
if (lastRect == null) return null;
int xOffset = customLayoutManager.getxOffset(); //拿到偏移量
if ((xOffset - lastRect.left) > (thisRect.left - xOffset)) { //哪个page距离屏幕边缘近,滚动到该page
return customLayoutManager.findViewByPosition(nextPageFistPosition);
} else {
return customLayoutManager.findViewByPosition(pageFistPosition);
}
}
return null;
}
/**
* 根据速度(velocityX)计算需要滚动到哪个Item
* @param layoutManager
* @param velocityX
* @param velocityY
* @return 目标Item对应的position
*/
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
Log.d("ccm", "findTargetSnapPosition");
if (layoutManager instanceof CustomLayoutManager) {
// Log.d("ccm","findTargetSnapPosition");
int childCount = layoutManager.getChildCount();
if (childCount <= 0) return RecyclerView.NO_POSITION;
//获取当前屏幕上最小的Item
int minItem = 9999;
for (int i = 0 ; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
int decoratedRight = layoutManager.getDecoratedRight(child);
int decoratedLeft = layoutManager.getDecoratedLeft(child);
if (decoratedRight > layoutManager.getPaddingLeft() && decoratedLeft < layoutManager.getWidth() - layoutManager.getPaddingRight()) {
int position = layoutManager.getPosition(child);
minItem = Math.min(minItem, position);
}
}
if (minItem == 9999) return RecyclerView.NO_POSITION;
CustomLayoutManager customLayoutManager = (CustomLayoutManager) layoutManager;
int tarPage = customLayoutManager.getTarPage(minItem); //计算出minItem对应的page
int maxPage = customLayoutManager.getMaxPage(); //获取总的page数量
if (velocityX > 0) {//左滑
if (tarPage < maxPage) {
//未到最后一页,滚动到下一页的第一个Item
return customLayoutManager.getPageFistPosition(tarPage + 1);
} else {
//已经到最后一页了,滚动到当前页的第一个Item
return customLayoutManager.getPageFistPosition(maxPage);
}
} else {
if (tarPage <= 1) {
//未到第一页,滚动到上一个页的第一个Item
return customLayoutManager.getPageFistPosition(1);
} else {
//已经到第一页了,滚动到第一页的第一个Iitem
return customLayoutManager.getPageFistPosition(tarPage);
}
}
} else {
return RecyclerView.NO_POSITION;
}
}
@Nullable
@Override
protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
return !(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider) ? null : new LinearSmoothScroller(mRecyclerView.getContext()) {
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
int dx = snapDistances[0];
int dy = snapDistances[1];
int time = this.calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, this.mDecelerateInterpolator);
}
}
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 25.0f / (float)displayMetrics.densityDpi;
}
protected int calculateTimeForScrolling(int dx) {
return Math.min(100, super.calculateTimeForScrolling(dx));
}
};
}
}
/** -------------------------CustomLayoutManager中需要添加几个辅助方法------------**/
public class CustomLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider{
/**
* 计算滚动到targetPosition需要滚动的距离
* @param targetPosition
* @return
*/
public PointF computeScrollForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
Rect rect = rectFList.get(targetPosition);
int offset = rect.left - mXOffset;
PointF pointF = new PointF(offset, 0);
return pointF;
}
/**
* 获取最多页数
* @return
*/
public int getMaxPage() {
int onePageCount = line * col;
return (int) Math.ceil(getItemCount()/(onePageCount + 0f));
}
/**
* 获取第page页第一个item的index
* @param page
* @return
*/
public int getPageFistPosition(int page) {
return line * col * (page - 1);
}
/**
* 根据position获取对应的Rect
* @param position
* @return
*/
public Rect getRectByPosition(int position) {
if (rectFList == null || rectFList.size() <= position) return null;
return rectFList.get(position);
}
}
使用CustomLayoutManager和customSnapHelper,实现ViewPager效果
recyclerView.setLayoutManager(new CustomLayoutManager(2,4));
new CustomSnapHelper().attachToRecyclerView(recyclerView);
recyclerView.setAdapter(new Adapter2());
最后,CustomLayoutManager和CustomSnapHelper的完整代码如下:
/** ---------------CustomLayoutManager--------------------**/
public class CustomLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider{
public CustomLayoutManager(int line, int col) {
this.line = line;
this.col = col;
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
}
/**
* 这个返回true可以自动根据所有Item的宽高,重新测试RecyclerView的宽高
* @return
*/
@Override
public boolean isAutoMeasureEnabled() {
return true;
}
int line = 2; //每一页line行
int col = 4; //每一行 col列
//每个item对应的位置信息
private List<Rect> rectFList = new ArrayList<>();
int mXOffset = 0; //当前滑动偏移量(scrollHorizontallyBy方法中记录的偏移量)
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler); //把Item从界面上剥离下来
int itemCount = getItemCount(); //获取item数量
if (itemCount <= 0) {
return;
}
View fistView = recycler.getViewForPosition(0);//获取第一个item
int width = getWidth();//获取RecyclerView的宽度
//这里考虑到RecyclerView可能有内边距,所以实际可用宽度应该是减去左右内边距
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
/**
* measureChildWithMargins方法的后两个参数是宽度和高度的使用量,
* 这里每一行是有4列,所以宽度的使用量应该是3/4 canUseWidth
*/
int usedWidth = Math.round(canUseWidth * ((col - 1f)/col));
int usedHeight = 0;//高度不受限制
//测量第一个Item,这里只测量第一个item,是默认所有的Item大小都是一样的
measureChildWithMargins(fistView,usedWidth, usedHeight);
//获取测量后Item的宽高
int measureWidth = getDecoratedMeasuredWidth(fistView);
int measureHeight = getDecoratedMeasuredHeight(fistView);
int colWidth = Math.round((canUseWidth + 0f) / col); //计算出每一列的宽度
//计算所有Item的位置情况
rectFList.clear();
for (int i = 0; i < itemCount; i++) {
int tarPage = getTarPage(i); //获取第i个Item对应的页码(1、2、3)
int tarLine = getTarLine(i); //获取对应的行码 (0、1、2、3)
int tarCol = getTarCol(i); //获取对应的列码(0、1、2、3)
//计算第tarPage页的偏移量 (每页宽度 * 页码) + 左边内边距
int xOffset = canUseWidth * (tarPage - 1) + getPaddingLeft();
//计算 第 i个Item的 上下左右位置
int left = colWidth * tarCol + xOffset;
int top = measureHeight * tarLine;
int right = left + colWidth;
int button = top + measureHeight;
Rect rect = new Rect(left, top,right, button);
rectFList.add(rect);
}
//计算当前界面上显示对应的区域
int pageStartX = mXOffset + getPaddingLeft();
int pageEndX = pageStartX + canUseWidth;
//缓存当前界面相邻一个page范围
pageStartX = pageStartX - canUseWidth;
pageEndX = pageEndX + canUseWidth;
//将所有的Item添加到RecyclerView中并设置对应的位置
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
//判断Item是否处于pageStartX到pageEndX区间内,如果是就把对应的Item添加到RecyclerView中去
if (rect.right > pageStartX && rect.left < pageEndX) {
View view = recycler.getViewForPosition(i); //回收池中获取Item
measureChildWithMargins(view, usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//设置对应Item在RecyclerView中的位置
int left = rect.left - mXOffset - getPaddingLeft(); //这里需要减去偏移量才是当前Item的位置
int top = rect.top;
int right = left + viewWidth;
int button = top + viewHeight;
Log.d("ccm" + i,String.format("left %d, right %d", left,right));
layoutDecorated(view, left, top, right, button);
}
}
}
/**
* 获取第position个Item在其所在Page对应的行数(0、1、... (line -1))
* @param position
* @return
*/
private int getTarLine(int position) {
int line = (int) Math.ceil((position + 1f) / this.col);
line = (line - 1)%this.line;
return (int) line;
}
/**
* 获取第position个Item在其所在Page对应的列数(0、1、... (col -1))
* @param position
* @return
*/
private int getTarCol(int position) {
int col = (int) Math.ceil((position) % this.col);
return col;
}
/**
* 获取第position个Item在所在的page(1、2、3、4.。。)
* @param position
* @return
*/
public int getTarPage(int position) {
int onePageCount = line * col;
return (int) Math.ceil((position + 1f) / onePageCount);
}
/**
* 需要支持左右滑动,所以这里需要返回true
* @return
*/
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
int itemCount = getItemCount(); //获取item数量
if (itemCount == 0) return 0;
//获取最后一个item对应的page值就等于获取到总的page数量
int pageCount = getTarPage(itemCount - 1);
int width = getWidth();//获取RecyclerView的宽度
//这里考虑到RecyclerView可能有内边距,所以实际可用宽度应该是减去左右内边距
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
int minOffset = 0;//最小滑动值是0
//每页宽度 * (页面数量 - 1) 得到最大滑动距离 (只有1页的时候是不能滑动的)
int maxOffset = canUseWidth * (pageCount - 1);
int realDx = dx; //真实滑动距离
if (mXOffset + dx < minOffset) { //如果滑动距离小于最小值,则修正真实滑动距离
realDx = 0 - mXOffset;
}
if (mXOffset + dx > maxOffset) { //如果滑动距离小于最大值,则修正真实滑动距离
realDx = maxOffset - mXOffset;
}
//根据偏移量计算当前显示区域
int pageStartX = mXOffset + getPaddingLeft();
int pageEndX = pageStartX + canUseWidth;
//根据realDx计算移动后的显示区域
pageStartX += realDx;
pageEndX += realDx;
//缓存界面相邻一个page范围
pageStartX -= canUseWidth;
pageEndX += canUseWidth;
//遍历已添加到RecyclerView上的Item,判断是否处于pageStartX到pageEndX范围内,如果不是
//给他移除掉添加到回收池中去
int childCount = getChildCount();//添加到界面上的Item数量
List<View> needRemoveView = new ArrayList<>(2);
for (int i = 0; i < childCount; i++) {
View item = getChildAt(i);
int position = getPosition(item);
Rect rect = rectFList.get(position);
if (!(rect.right > pageStartX && rect.left < pageEndX)) {
//不处于pageStartX到pageEndX范围内的Item需要移除
needRemoveView.add(item);
}
}
for (View item : needRemoveView) {
removeAndRecycleView(item, recycler); //移除掉
}
//添加Item
int usedWidth = Math.round(canUseWidth * ((col - 1f)/col));
int usedHeight = 0;//高度不受限制
for (int i = 0; i < itemCount; i++) {
Rect rect = rectFList.get(i);
//判断Item是否处于pageStartX到pageEndX区间内,如果是就把对应的Item添加到RecyclerView中去
if (rect.right > pageStartX && rect.left < pageEndX) {
View view = null;
//查找该Item是否已经在界面上了
view = findViewByPosition(i);
if (view == null) { //界面上查找不到,说明需要添加
view = recycler.getViewForPosition(i); //回收池中获取Item
measureChildWithMargins(view, usedWidth, usedHeight);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
addView(view);//添加到RecyclerView中去
//设置对应Item在RecyclerView中的位置
int left = rect.left - mXOffset - getPaddingLeft(); //这里需要减去偏移量才是当前Item的位置
int top = rect.top;
int right = left + viewWidth;
int button = top + viewHeight;
layoutDecorated(view, left, top, right, button);
}
}
}
//计算新的mXOffset
mXOffset = mXOffset + realDx;
//执行滑动操作
offsetChildrenHorizontal(-realDx);
//返回值是本次滑动的真实距离,RecyclerView是支持嵌套滚动的,这里涉及到嵌套滚动的内容就不做赘述了
return realDx;
}
@Nullable
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
Rect rect = rectFList.get(targetPosition);
int offset = rect.left - mXOffset;
PointF pointF = new PointF(offset, 0);
return pointF;
}
/**
* 计算滚动到targetPosition需要滚动的距离
* @param targetPosition
* @return
*/
public PointF computeScrollForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
Rect rect = rectFList.get(targetPosition);
int offset = rect.left - mXOffset;
PointF pointF = new PointF(offset, 0);
return pointF;
}
/**
* 获取最多页数
* @return
*/
public int getMaxPage() {
int onePageCount = line * col;
return (int) Math.ceil(getItemCount()/(onePageCount + 0f));
}
/**
* 获取第page页第一个item的index
* @param page
* @return
*/
public int getPageFistPosition(int page) {
return line * col * (page - 1);
}
/**
* 根据position获取对应的Rect
* @param position
* @return
*/
public Rect getRectByPosition(int position) {
if (rectFList == null || rectFList.size() <= position) return null;
return rectFList.get(position);
}
public int getxOffset() {
return mXOffset;
}
}
/** --------------------------------------CustomSnapHelper--------------**/
public class CustomSnapHelper extends SnapHelper {
private RecyclerView mRecyclerView;
@Override
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
this.mRecyclerView = recyclerView;
super.attachToRecyclerView(recyclerView);
}
/**
* 计算滚动到targetView需要滚动的距离
* @param layoutManager
* @param targetView
* @return
*/
@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
Log.d("ccm", "calculateDistanceToFinalSnap");
if (layoutManager instanceof CustomLayoutManager) {
CustomLayoutManager customLayoutManager = (CustomLayoutManager) layoutManager;
int position = customLayoutManager.getPosition(targetView); //得到targetView对应的position
PointF pointF = customLayoutManager.computeScrollForPosition(position); //计算需要滚动距离
int[] a = new int[2];
a[0] = Math.round(pointF.x);
a[1] = Math.round(pointF.y);
return a;
} else {
int[] a = new int[2];
a[0] = 0;
a[1] = 0;
Log.d("ccm", "calculateDistanceToFinalSnap");
return a;
}
}
/**
* 计算出当前页面需要对齐的Item
* @param layoutManager
* @return
*/
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof CustomLayoutManager) {
int childCount = layoutManager.getChildCount();
if (childCount <= 0) return null;
//获取当前屏幕上最小的Item
int minItem = 9999;
for (int i = 0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
int decoratedRight = layoutManager.getDecoratedRight(child);
int decoratedLeft = layoutManager.getDecoratedLeft(child);
if (decoratedRight > layoutManager.getPaddingLeft() && decoratedLeft < layoutManager.getWidth() - layoutManager.getPaddingRight()) {
int position = layoutManager.getPosition(child);
minItem = Math.min(minItem, position);
}
}
if (minItem == 9999) return null;
CustomLayoutManager customLayoutManager = (CustomLayoutManager) layoutManager;
int tarPage = customLayoutManager.getTarPage(minItem);//计算出minItem对应的page
int maxPage = customLayoutManager.getMaxPage(); //获取总的page数量
int pageFistPosition = customLayoutManager.getPageFistPosition(tarPage); //获取当前page的第一个Item
if (tarPage == maxPage) { //如果当前pege已经是最后一页了,返回当前page的第一个Item
return customLayoutManager.findViewByPosition(pageFistPosition);
}
int nextPageFistPosition = customLayoutManager.getPageFistPosition(tarPage + 1); //获取下一个page的第一个Item
//得到当前page的第一个Item和下一个page的第一个Item 对应的位子数据
Rect lastRect = customLayoutManager.getRectByPosition(pageFistPosition);
if (lastRect == null) return null;
Rect thisRect = customLayoutManager.getRectByPosition(nextPageFistPosition);
if (lastRect == null) return null;
int xOffset = customLayoutManager.getxOffset(); //拿到偏移量
if ((xOffset - lastRect.left) > (thisRect.left - xOffset)) { //哪个page距离屏幕边缘近,滚动到该page
return customLayoutManager.findViewByPosition(nextPageFistPosition);
} else {
return customLayoutManager.findViewByPosition(pageFistPosition);
}
}
return null;
}
/**
* 根据速度(velocityX)计算需要滚动到哪个Item
* @param layoutManager
* @param velocityX
* @param velocityY
* @return 目标Item对应的position
*/
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
Log.d("ccm", "findTargetSnapPosition");
if (layoutManager instanceof CustomLayoutManager) {
// Log.d("ccm","findTargetSnapPosition");
int childCount = layoutManager.getChildCount();
if (childCount <= 0) return RecyclerView.NO_POSITION;
//获取当前屏幕上最小的Item
int minItem = 9999;
for (int i = 0 ; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
int decoratedRight = layoutManager.getDecoratedRight(child);
int decoratedLeft = layoutManager.getDecoratedLeft(child);
if (decoratedRight > layoutManager.getPaddingLeft() && decoratedLeft < layoutManager.getWidth() - layoutManager.getPaddingRight()) {
int position = layoutManager.getPosition(child);
minItem = Math.min(minItem, position);
}
}
if (minItem == 9999) return RecyclerView.NO_POSITION;
CustomLayoutManager customLayoutManager = (CustomLayoutManager) layoutManager;
int tarPage = customLayoutManager.getTarPage(minItem); //计算出minItem对应的page
int maxPage = customLayoutManager.getMaxPage(); //获取总的page数量
if (velocityX > 0) {//左滑
if (tarPage < maxPage) {
//未到最后一页,滚动到下一页的第一个Item
return customLayoutManager.getPageFistPosition(tarPage + 1);
} else {
//已经到最后一页了,滚动到当前页的第一个Item
return customLayoutManager.getPageFistPosition(maxPage);
}
} else {
if (tarPage <= 1) {
//未到第一页,滚动到上一个页的第一个Item
return customLayoutManager.getPageFistPosition(1);
} else {
//已经到第一页了,滚动到第一页的第一个Iitem
return customLayoutManager.getPageFistPosition(tarPage);
}
}
} else {
return RecyclerView.NO_POSITION;
}
}
@Nullable
@Override
protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
return !(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider) ? null : new LinearSmoothScroller(mRecyclerView.getContext()) {
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(layoutManager, targetView);
int dx = snapDistances[0];
int dy = snapDistances[1];
int time = this.calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, this.mDecelerateInterpolator);
}
}
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 25.0f / (float)displayMetrics.densityDpi;
}
protected int calculateTimeForScrolling(int dx) {
return Math.min(100, super.calculateTimeForScrolling(dx));
}
};
}
}