注*以下都是来自Google官方的自定义LayoutManager文档,在学习的过程中整理出来的。
原文地址是:http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/。在此之前了解一下RecyclerView的缓存机制,对于理解此文会事半功倍。
这个是我学习RecyclerView的一篇文章
https://blog.csdn.net/weixin_43130724/article/details/90068112
错误的地方还请指出谢谢啦。
首先,先深入了解一下Api的结构:
*
The Recycler:
- 需要回收旧view或从可能回收的前一个子view中获取新view时,layoutManager将在流程的关键点上访问recycler的实例。
getViewPostition():
当layoutManager需要一个新的子view的时候,只需调用getViewForPosition(),Recycler将返回已经绑定了适当数据的view。Recycler负责确定是否必须创建新view,或者是重用现有的废弃view。在layoutManager中,应确保不再可见的视图及时传递给Recycler,这样可以避免Recycler创建不必要的view对象。
## Detach 和 Remove
- 在更新view期间,有两种方法处理现有的子view:Detach和Remove。Detach是轻量级的重新排序view的操作,被Detach的layout将在代码返回之前被重新附加。可以用来修改附加子view的索引,无需通过Recycler重新绑定或重新创建这些view(也就是说:被Detach的viewHolder可以直接拿来使用不必再经过onBindViewHoder()和onCreateViewHolder()这两个方法。)。
Remove则被用于不再需要或不再长时间需要的view。任何永久删除的view都应该放在Recycler中,以供以后重用,但是Api没有强制执行这一点。自己删除的view是否被回收,取决于您自己。
# Scrap and Recycle Pool:
- 这是Recycler的两级视图缓存系统。Scrap是一个轻量级的集合,view可以直接返回到layoutManager,而不必在经过适配器。被Detach的view通常会被放在着,将在相同的layout传递中重用。Recycle Pool里面的是各种view都在这边(数据不正确的,被Remove的),再次重用的时候需重新绑定。
当想要向LayoutManager提供一个新的view的时候,最先搜索的就是Scrap中是否有相匹配的Position/id,如果有就返回不用重新绑定到适配器数据。如果没有找到相匹配的,Recycler 就会在pool中提取一个合适的,并重新绑定一个必要的数据(onBindViewHoder())。如果在循环池中没有有效的view就会创建一个新的(onCreateViewHolder())。
Rule of Thumb
- LayoutManager Api 允许独立完成几乎所有的任务。通常对于希望临时重新组织并且在相同的布局传递中重新附加的view可以使用detachAndScrapView(),对于当前基本不需要的,可以使用removeAndRecyclingView()。
Core:
generateDefaultLayoutParams():
LayoutManager可以实现实时的附加,测量和布局所需要的所有子view,当用户滚动视图的时候,由layoutManager决定何时需要添加新的子view合适可以Detach,scrap,
generateDefaultLayoutParams(),这是自定义layoutManager必须重写的一个方法。只需要返回一个recycle视图的新实例,在getViewForPosition()返回之前,这些参数将应用于每个子元素。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,RecyclerView.LayoutParams.WRAP_CONTENT);
}
**
onLayoutChildren():
这是layoutManager的主要入口点,当需要初始化布局时就会调用此方法,当适配器数据集发生更改也会调用此方法。对于布局的任何更改都不会调用此方法。这是为初始传递数据或数据集更改的好机会。
在下一节将会研究如何在适配器更新时根据当前可见的元素使用此方法进行布局,下面这是GridLayoutManager实列的简化版本:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//测量第一个子view
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
//测量第一个子View的宽高因为他们不会改变
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
detachAndScrapView(scrap, recycler);
updateWindowSizing();
int childLeft;
int childTop;
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
//将所有附加视图清除到回收站
detachAndScrapAttachedViews(recycler);
//填充初始布局的网格
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler);}
为了简单起见这个管理器假设适配器中的所有子视图大小相同,并确保所有可能已经存在的视图都在个管理器假设适配器中的所有子视图大小相同并确保所有可能已经存在的视图都在scrap 中,大部分的工作都在fillGrid()这个方法中以备重用,当滚动发生时,这个方法会被大量调用来更新可见视图。
一般来说用这个方法主要步骤是:
1.检查所有附加的view在最新滚动事件之后的偏移位置。
2.确定在需要添加view的时候是否创建了空白,可以从Recycler中获得。
3.确定是否有不可见的view,把他们放到Recycler中
4.是否应该重新组织view
FixedGridLayoutManager.fillGrid():该方法管从右到左排序位置,当达到最大列数时进行包装:
1.将它们暂时detach,以便稍后可以重新连接。
SparseArray viewCache = new SparseArray(getChildCount());//...
if (getChildCount() != 0) {
//...
//缓存所有i子temView
for (int i=0; i < getChildCount(); i++) {
int position = positionOfIndex(i);
final View child = getChildAt(i);
viewCache.put(position, child);
}
//detach掉所有的item
for (int i=0; i < viewCache.size(); i++) {
detachView(viewCache.valueAt(i));
}}
2.为当前可见的每个子视图测量/布局 ,从Recycler那里重新获得。
for (int i = 0; i < getVisibleChildCount(); i++) {
//...
//Layout this position
View view = viewCache.get(nextPosition);
if (view == null) {
/* 如果没有合适的view可以用,Recycler就会返回新创建的,或者是返回一个适当的view并且绑定好了数据 */
view = recycler.getViewForPosition(nextPosition);
addView(view);
/* 如果是一个新创建的就需要重新测量和布局 ,从recycler中获得就不用 */
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, leftOffset, topOffset,
leftOffset + mDecoratedChildWidth,
topOffset + mDecoratedChildHeight);//left,top,right,bottom
} else {
//Re-attach the cached view at its new index
attachView(view);
viewCache.remove(nextPosition);
}
//...}
3.不再可见的都会被放入Recycler中,可见的已经被重新布局了(step2)。
for (int i=0; i < viewCache.size(); i++) {
recycler.recycleView(viewCache.valueAt(i));}
另外,先detach再重新添加的原因是为了保持子索引的顺序(即getChildAt()索引)。我们希望可视视图从左上角的0流到右下角的getChildCount()-1。当我们在两个方向滚动并附加了新的子元素时,这种顺序将变得不可靠。我们需要保留这个顺序,以便在任何时候最好地确定每个子元素的位置。在更简单的LayoutManager(如LinearLayoutManager)中,可以很容易地将子视图插入列表的每一端。
Adding User Interactivity 添加用户交互性
上面的布局是完成了但是不能滑动,因此需要重写canScrollHorizontally() & canScrollVertically(),scrollHorizontallyBy() & scrollVerticallyBy() 这几个方法个方法。canScrollHorizontally() & canScrollVertically(),前者是是否水平滑动,后者是垂直滑动
返回true or false 行了
@Override
public boolean canScrollVertically() {
//是否垂直方向滑动
return true;}
scrollHorizontallyBy() & scrollVerticallyBy()
主要是,处理子view移动的距离,在滑动后是否添加/删除itemveiw和确定上下左右边界。
RecyclerView已经处理了滚动和投掷触摸逻辑,因此不会干扰MotionEvents或手势检测器。
在FixedGridLayoutManager中,这两种方法非常相似,下面是垂直方向的实现:
@Override
public int scrollVerticallyBy(int dy,
RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
//获取第一个和最后一个itemView的对象
final View topView = getChildAt(0);
final View bottomView = getChildAt(getChildCount()-1);
//防止数据过少就无法滚动。(就是说如果itemview只有几个不能溢出屏幕的范围,就不能动)
int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView);
if (viewSpan <= getVerticalSpace()) {
return 0;
}
int delta;//滚动距离
int maxRowCount = getTotalRowCount();//下边界
boolean topBoundReached = getFirstVisibleRow() == 0;//上边界
boolean bottomBoundReached = getLastVisibleRow() >= maxRowCount;
if (dy > 0) { // 垂直方向,向上滚动>0,向下滚动<0
if (bottomBoundReached) {
//如果已经到最后一行了就限制
int bottomOffset;
if (rowOfIndex(getChildCount() - 1) >= (maxRowCount - 1)) {
//当已经在最后一行的时候,就设置下界
bottomOffset = getVerticalSpace()
- getDecoratedBottom(bottomView) + getPaddingBottom();
} else {
/* 当剩下的位置不够在容下一个item View 的时候,就留白 */
bottomOffset = getVerticalSpace()
- (getDecoratedBottom(bottomView) + mDecoratedChildHeight)
+ getPaddingBottom();
}
delta = Math.max(-dy, bottomOffset);
} else {
/没有达到下届就不限制
delta = -dy;
}
} else { //<0的情况,检查上界
if (topBoundReached) {
int topOffset = -getDecoratedTop(topView) + getPaddingTop();
delta = Math.min(-dy, topOffset);
} else {
delta = -dy;
}
}
offsetChildrenVertical(delta);
if (dy > 0) {
if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) {
fillGrid(DIRECTION_DOWN, recycler);
} else if (!bottomBoundReached) {
fillGrid(DIRECTION_NONE, recycler);
}
} else {
if (getDecoratedTop(topView) > 0 && !topBoundReached) {
fillGrid(DIRECTION_UP, recycler);
} else if (!topBoundReached) {
fillGrid(DIRECTION_NONE, recycler);
}
}
/* *返回值决定是否达到边界 */
return -delta;}
offsetChildrenVertical() and offsetChildrenHorizontal(),如果滚动的距离覆盖了内容的边缘就需要,减少视图实际移动的距离,需要手动把视图移动到这个方法中。如果不这样做,视图就不会滚动。在移动视图之后,我们触发另一个填充操作,根据滚动的方向交换视图。
最后,返回应用于子元素的实际位移值。clerview使用这个值来确定什么时候应该绘制您在到达滚动内容末尾时看到的边缘效果。本质上,如果返回值与传入的dx/dy不完全匹配,则需要绘制一定数量的边缘辉光。还需要注意的是,如果返回的值符号不正确,框架的数学计算也会将其作为一个大的变化,就会在错误的时间得到边缘发光。
除了绘制边缘效果之外,这个返回值还用于确定何时取消flings。在这里返回不正确的值将禁用您抛出内容的能力,因为框架会认为过早的到达了边界然后就不能动了。