FormLayoutManager首页,里面有github地址
目录
前言
接下来会一步步带大家走进FormLayoutManager。在写这篇博客之前,我已经实现可以不同行或不同列多类型布局。所以下面讲解是以最新的v2.0代码来说明,后面也会介绍到怎么实现多类型。FormLayoutManager的完整代码有点长,我就不贴出来,大家可以自己看着源码,一边看文章,我们由上至下来说。
构造方法
public FormLayoutManager(int columnCount){
mColumnCount = columnCount;
}
public FormLayoutManager(boolean isHorV, int count){
this(isHorV, count, null);
}
/**
* 什么场景需要传入RecyclerView
* 在滚动过程会刷新的数据的时候,最好设置RecyclerView
*/
public FormLayoutManager(int columnCount, RecyclerView recyclerView){
this(true, columnCount, recyclerView);
}
public FormLayoutManager(boolean isHorV, int count, RecyclerView recyclerView){
mIsHorV = isHorV;
if (isHorV){
mColumnCount = count;
}else{
mRowCount = count;
}
mRecyclerView = recyclerView;
}
isHorV是标志是水平表格还是垂直表格。默认是大家常用的水平表格,最基本的构造方法就是要传入列数。也提供一个构造方法让大家设置isHorV,来告诉FormLayoutManager你的表格水水平表格还是垂直表格,而当时垂直表格时,传入的count就代表列数了。
private int getColumnCount() {
if (mIsHorV){
return mColumnCount;
}else{
return (getItemCount() - 1) / mRowCount + 1;
}
}
目光来到getColumnCount,当为水平表格时,列数就是我们构造函数传进来的那个columnCount的值。而当它为垂直表格的时候,我们列数是要根据行数计算。行数的getRowCount同理。
目光请有回到构造函数那里,其中有两个方法是要传入RecyclerView。注释有解析到如果你的表格在你滚动情况下也会突然刷新数据,建议设置一下RecyclerView,因为刷新数据的时候,我们希望表格刷新的是当前可以看到的view,用了RecyclerView来执行的刷新方法比较快一点,后面再细说。
onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mItemCount != getItemCount()){
mItemCount = 0;
}
if (mItemCount == 0 || mRecyclerView == null){
mItemCount = getItemCount();
handleLayoutChildren(recycler);
}else{
// 防止数据在更新的时候,用户又在滑动表格,这时会看到卡顿现象
// 第二种刷新当前界面可视的view,不过要设置RecyclerView,但刷新时间短
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int position = getPosition(child);
mRecyclerView.getAdapter().onBindViewHolder(mRecyclerView.getChildViewHolder(child), position);
}
}
}
我们先来看else下面的方法,这段代码就是整个FormLayoutManager里面唯一一段用到RecyclerView的代码。这段代码很好理解,就是遍历界面可视的item数getChildCount,然后逐个调用onBindViewHolder来实现刷新该item。其实if下面的handleLayoutChildren里面也是只会刷新当前可视的item,所以整个FormLayoutManager就算你不设置RecyclerView,是一样可以正常用的。我只是考虑,但表格的总item数不变且要刷新数据的时候,没必要执行一个handleLayoutChildren这么庞大的一段逻辑,直接用else下面的方法更快。
if (mItemCount != getItemCount()){
mItemCount = 0;
}
上面这个让mItemCount归0是干什么的呢?因为要执行handleLayoutChildren的判断条件是mItemCount等于0,而handleLayoutChildren里面初始化了好一些变量,所以当我们刷新数据,导致总item的数目发生变化的时候,必须要调用handleLayoutChildren来重新计算初始化那些变量。
handleLayoutChildren
1、计算每个item的Rect
接下来看一下handleLayoutChildren里面第一个大的for循环干了什么
for (int i = 0; i < getItemCount(); i++) {
// item所在的行和列的index
int row = i / mColumnCount;
int column = i % mColumnCount;
View itemView = recycler.getViewForPosition(i);
Integer itemViewType = getItemViewType(itemView);
int itemWidth;
int itemHeight;
if (mItemViewSizeMap.containsKey(itemViewType)){
itemWidth = mItemViewSizeMap.get(itemViewType).width;
itemHeight = mItemViewSizeMap.get(itemViewType).height;
}else{
measureChildWithMargins(itemView, 0, 0);
itemWidth = getDecoratedMeasuredWidth(itemView);
itemHeight = getDecoratedMeasuredHeight(itemView);
mItemViewSizeMap.put(itemViewType, new ItemViewSize(itemWidth, itemHeight));
}
Rect rect = getViewRect(row, column, itemWidth, itemHeight);
mItemRects.add(rect);
mHasAttachedItems.add(false);
}
上面是遍历了总item的个数,之前没做多类型表(多类型表指不同行或不同列多类型)的时候,获取itemWidth和itemHeight非常简单,直接拿position为0的第一个item的宽高就行了,因为没多类型,故每个格子的宽高都一样。这个新写的for循环是考虑到多类型的情况。
LayoutManager自带有个getItemViewType获取item类型的,然后我们有个字典mItemViewSizeMap来保存不同类item的宽高。最关键是后面getViewRect,我先说一下mItemRects里面保存的是什么,这个列表里面保存的rect,其实是在不滚动的情况下,所有item(不管可视还是不可视)在界面的位置。之前没多类型,每个格子宽高一样时候,每个item的rect很好算的,就是按循环累加一下itemWidth或itemHeight就行了,现在多了多类型,就写了一个getViewRect的方法,下面我们看一下里面写了什么。
// 获取view对应的Rect
private Rect getViewRect(int row, int column, int itemWidth, int itemHeight) {
int left = 0;
int right = itemWidth;
int top = 0;
int bottom = itemHeight;
if (mItemRects.size() > 0){
Rect lastRect = mItemRects.get(mItemRects.size() - 1);
if (mCurRow != row){
mCurRow = row;
left = 0;
right = itemWidth;
top = lastRect.bottom;
bottom = top + itemHeight;
}else if (mCurColumn != column){
mCurColumn = column;
left = lastRect.right;
right = left + itemWidth;
top = lastRect.top;
bottom = lastRect.bottom;
}
}
Rect rect = new Rect(left, top, right, bottom);
return rect;
}
上面可以看到当mItemRects.size() > 0才会执行一段计算,当它不大于0的时候,其实就是在计算第一个item的rect,它的对应属性其实就是一开始的默认值
int left = 0;
int right = itemWidth;
int top = 0;
int bottom = itemHeight;
而其他的rect就要通过if下面的代码计算了。先跟大家说明一下我们的所有item要是描绘在整个表格,它是按什么顺序把item加进去。答案就是从左到右,然后上到下,就是一个个item从左到右地加进去,一行加满就向下换行,继续左到右。好,那么就来看if下面的代码了。
if (mCurRow != row){
mCurRow = row;
left = 0;
right = itemWidth;
top = lastRect.bottom;
bottom = top + itemHeight;
}
当mCurRow不等于row,即当前行不等于传进来的行时,证明开始换行,那换行的第一个item,好明显left就是0,right就是itemWidth啦。而关键的top其实就是上一个rect的bottom了,上一个的rect其实就是它的上一行的最后一个item的rect,而bottom是top加itemHeight就不多说了。那接下来
else if (mCurColumn != column){
mCurColumn = column;
left = lastRect.right;
right = left + itemWidth;
top = lastRect.top;
bottom = lastRect.bottom;
}
else if /—~—/,没错就是else if,当行相等的时候才会进入,然后判断列是否相等,不相等就说明换列,因为是同行换列,故left就是上一个rect的right,而right就不多说了,top和bottom跟上一个rect的一样。这样就准确记录了每个item的rect了。
2、绘制每个可视的item
here:
for (int row = firstShowRow; row < visibleRowCount + firstShowRow; row++) {
for (int column = firstShowCol; column < visibleColumnCount + firstShowCol; column++) {
int itemPosition = row * mColumnCount + column;
if (itemPosition >= mItemRects.size()){
break here;
}
Rect rect = mItemRects.get(itemPosition);
if (!Rect.intersects(getVisibleArea(), rect)){
continue;
}
View view = recycler.getViewForPosition(itemPosition);
addView(view);
//addView后一定要measure,先measure再layout
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, rect.left - mSumDx, rect.top - mSumDy, rect.right - mSumDx, rect.bottom - mSumDy);
}
}
firstShowRow, firstShowCol, visibleColumnCount, visibleRowCount这四个值怎么得来,前面几段代码,我相信你能看明白。我主要说一下我贴出来的这段代码。为什么要获取第一个可视item的行和列呢(firstShowRow和firstShowCol),就是因为这两个值,才让handleLayoutChildren每次刷新,也只会刷新当前可视的item。
两个for循环为了遍历的就是可视区的所有item,我们着重看一下循环里面的代码,第一句其实就是把行和列转换成该item对应的position。先跳开两个判断,浅谈下面绘制view的代码,很明确,就是在recycler,那里根据itemPosition获取对应的itemView,然后调layoutDecorated绘制到RecyclerView上。
第一个判断,为什么itemPosition会可能大于mItemRects.size()。我们来看一下visibleColumnCount, visibleRowCount怎么算的。
// 可视的最大行数,列数
int visibleRowCount = (int) Math.ceil(getVerticalSpace() * 1f / minHeight) + 1;
int visibleColumnCount = (int) Math.ceil(getHorizontalSpace() * 1f / minWidth) + 1;
拿可视最大列数作分析,看下面左图,我们那RecyclerView可视的宽getHorizontalSpace除以item的宽,得到的结果应该是2点多,然后我们向上取整Math.ceil得到的就是3,但往往这个结果都要加1,。因为只要你左滑一下就可以发现,最多可视的列其实是4列,如右图
关键是我拿来计算的不是item的width,而是所有item最小的minWidth(minWidth怎么得到的,看源码很容易理解的)。之前没多类型的时候,每个格子宽高一样,那个可视最大数是算得很准的。但由于加了多类型,那就是能考虑极端的情况,假设所有item都是最小那个width来计算可视最大数。
回到我们的两个for循环,因此visibleColumnCount, visibleRowCount可能会比实际的大,所以两个for循环所遍历的itemPosition会比实际表格的itemPosition大,有可能会越界,所以才有第一个判断出现,当这个itemPosition已经超过表格最大的那个position,我们就直接跳出两个循环。接下来看第二个判断:
Rect rect = mItemRects.get(itemPosition);
if (!Rect.intersects(getVisibleArea(), rect)){
continue;
}
大家可以打开demo看第一个界面,第一个界面的表格上面的数字其实就是格子对应的itemPosition。
看图,不难发现可视区为起始position=16,4X7的一个表,而上面我们说了,两个for循环遍历的范围比我们可视的还大。比如两个for循环遍历可能会出现position=20,但这个格子根本就不在我们的可视区,所以存在第二个判断,当判断这个position对应的rect不在我们的可视区的时候,就继续contiunue。
handleLayoutChildren的内容基本说完了,接下来总结一下这篇博客。
总结
FormLayoutManager内容太多了,所以分几篇来说明。这篇主要还是说了onLayoutManager这个方法的重写。而我们的表格进行了什么操作才会进入onLayoutManager呢?当我们的RecyclerView从gone到可视,还有adapter数据刷新的时候,都会进这方法。这个方法尤其重要,是FormLayoutManager的主入口来的。
下一篇:FormLayoutManager -- 解说(2)还没写……
来源:CSDN
作者:DNWalter
链接:https://blog.csdn.net/DNWalter/article/details/103869829