https://www.jianshu.com/p/60aa2fc17870
Foreword
Last week I mentioned in the article "RecyclerView-LayoutManager" that the GridLayoutManager
following homepage can be realized by using :
Some students expressed interest in this, but there is no ready-made case, so I simply implemented one by myself. The final result is as follows:
effect
I believe that many students have the same feeling as me, thinking that GridLayoutManager
only standard grid layout can be achieved. It was not until I decided to study some time ago RecyclerView
and read GridLayoutManager
the source code that I found out that it can do more things, for example, write one Home page.
Before reading this article, you need some knowledge reserves :
View
Have some simple understanding of the drawing process.Canvas
Simple and practical.RecyclerView+GridLayoutManager
usage of.
table of Contents
table of Contents
One, the scene
Use RecyclerView
+ GridLayoutManager
+ ItemDecoration
Customized homepage applicable scenarios:
- There are multiple functional modules
- Multiple styles of subviews
- The last module needs to be refreshed (if there is such a function, it must be
RecyclerView
realized), for example, QQ music slides down to recommend music that users may be interested in.
I personally feel that the significance of this solution is to reduce the nesting of the layout and make the interface management easier , but it may not be suitable for particularly complex business situations.
2. Thinking
Two difficulties need to be solved to realize the above functions :
- How to show different numbers of subviews for different rows
- Drawing of each module title
The solutions to these two problems correspond to GridLayoutManager
and respectively ItemDecoration
, we understand one by one.
1. GridLayoutManager
GridLayoutManager
In fact, we are already familiar with it, but we don’t usually understand SpanSize
this concept. Let’s look at the following piece of code:
GridLayoutManager gll = new GridLayoutManager(this, 6);
mRecyclerView.setLayoutManager(gll);
In the above code, we created a vertical view with a maximum capacity of 6 child views per row GridLayoutManager
. By default, the total number SpanSize
of one row is 6, and the default value of each child view SpanSize
is 1. Therefore, GridLayoutManager
each row will be divided into 6 without processing. Each copy shows a sub-view, as shown in the first row of the following figure:
style
At this time, if I SpanSize
set all the subviews to 2, then this subview will occupy the entire available width of the RecyclerView 2/6
, as shown in the second row of the above figure. Similarly, I will SpanSize
rise to 3, then the width of the subview will also rise For the available width 3/6
, as shown in the third row of the above figure, this is also GridLayoutManager
the reason why different numbers of subviews can be set in different rows. Of course, you can also set the three subviews in the same row SpanSize
to 1, 2, and 3.
Okay, how to draw the title is one step away from actual code combat.
2. ItemDecoration
The dividing line ItemDecoration
is a very interesting thing, because it can achieve some fun things, such as the following letter titles and timeline of the address book :
Letter Title of Address Book
Timeline
You can also use it to do some special effects, such as the ceiling of letter titles. Here I recommend two libraries:
- Vivian's timeline TimeLine
- The title of the address book of mcxtzhang, can realize the ceiling SuspensionIndexBar
Here is a brief introduction to ItemDecoration
the principle. Here I assume that View
the surveying and mapping process that the students have already understood is mainly divided into two parts:
- Draw the dividing line below the
RecyclerView
sub-view, because theItemDecoration
first drawing method of the dividing lineItemDecoration#onDraw
occurs before drawing theRecyclerView
sub-view. If you want it to be displayed, you need toItemDecoration
set an offset to offset the sub-view so that it will not be blocked.ItemDecoration
. - Draw the dividing line on
RecyclerView
the upper layer of the sub-view, because the drawing methodItemDecoration#onDrawOver
takes place after the drawing of theRecyclerView
sub-view is completed, whichItemDecoration
can also achieve the ceiling effect.
Three, code combat
With the knowledge reserve above, the following is simple.
1. Custom ItemDecoration
Custom ItemDecoration
three methods need to be implemented, with the related principle that we mentioned above:
Method name | Explanation |
---|---|
onDraw |
Draw the dividing line under the subview |
getItemOffsets |
The space usually reserved for displaying the lower divider |
onDrawOver |
Draw the upper divider |
Our task is only to draw a title, so the above two methods are enough.
1.1 Define data interface
/**
* 数据约束
*/
public interface IGridItem {
/**
* 是否启用分割线
* @return true
*/
boolean isShow();
/**
* 分类标签
*/
String getTag();
/**
* 权重
*/
int getSpanSize();
}
1.2 Custom ItemDecoration class
The core code is more than 100 lines:
/**
* 适用于GridLayoutManager的分割线
*/
public class GridItemDecoration extends RecyclerView.ItemDecoration {
// 记录上次偏移位置 防止一行多个数据的时候视图偏移
private List<Integer> offsetPositions = new ArrayList<>();
// 显示数据
private List<? extends IGridItem> gridItems;
// 画笔
private Paint mTitlePaint;
// 存放文字
private Rect mRect;
// 颜色
private int mTitleBgColor;
private int mTitleColor;
private int mTitleHeight;
private int mTitleFontSize;
private Boolean isDrawTitleBg = false;
private Context mContext;
// 总的SpanSize
private int totalSpanSize;
private int mCurrentSpanSize;
//... 省略一些方法
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
// 绘制标题的逻辑:
// 如果该行的数据的需要显示的标题不同于上行的标题,就绘制标题
final int paddingLeft = parent.getPaddingLeft();
final int paddingRight = parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int pos = params.getViewLayoutPosition();
IGridItem item = gridItems.get(pos);
if (item == null || !item.isShow())
continue;
if (i == 0) {
drawTitle(c, paddingLeft, paddingRight, child
, (RecyclerView.LayoutParams) child.getLayoutParams(), pos);
} else {
IGridItem lastItem = gridItems.get(pos - 1);
if (lastItem != null && !item.getTag().equals(lastItem.getTag())) {
drawTitle(c, paddingLeft, paddingRight, child,
(RecyclerView.LayoutParams) child.getLayoutParams(), pos);
}
}
}
}
/**
* 绘制标题
*
* @param canvas 画布
* @param pl 左边距
* @param pr 右边距
* @param child 子View
* @param params RecyclerView.LayoutParams
* @param pos 位置
*/
private void drawTitle(Canvas canvas, int pl, int pr, View child, RecyclerView.LayoutParams params, int pos) {
if (isDrawTitleBg) {
mTitlePaint.setColor(mTitleBgColor);
canvas.drawRect(pl, child.getTop() - params.topMargin - mTitleHeight, pl
, child.getTop() - params.topMargin, mTitlePaint);
}
IGridItem item = gridItems.get(pos);
String content = item.getTag();
if (TextUtils.isEmpty(content))
return;
mTitlePaint.setColor(mTitleColor);
mTitlePaint.setTextSize(mTitleFontSize);
mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD);
mTitlePaint.getTextBounds(content, 0, content.length(), mRect);
float x = UIUtils.dip2px(20f);
float y = child.getTop() - params.topMargin - (mTitleHeight - mRect.height()) / 2;
canvas.drawText(content, x, y, mTitlePaint);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
// 预留逻辑:
// 只要是标题下面的一行,无论这行几个,都要预留空间给标题显示
int position = parent.getChildAdapterPosition(view);
IGridItem item = gridItems.get(position);
if (item == null || !item.isShow())
return;
if (position == 0) {
outRect.set(0, mTitleHeight, 0, 0);
mCurrentSpanSize = item.getSpanSize();
} else {
if (!offsetPositions.isEmpty() && offsetPositions.contains(position)) {
outRect.set(0, mTitleHeight, 0, 0);
return;
}
if (!TextUtils.isEmpty(item.getTag()) && !item.getTag().equals(gridItems.get(position - 1).getTag())) {
mCurrentSpanSize = item.getSpanSize();
} else
mCurrentSpanSize += item.getSpanSize();
if (mCurrentSpanSize <= totalSpanSize) {
outRect.set(0, mTitleHeight, 0, 0);
offsetPositions.add(position);
}
}
}
}
The general logic is:
- If the position of the
RecyclerView
sub-view is below the title, you need to reserve space, and set it in theoutRect
middle. It should be noted that multiple sub-views in the same row need to reserve space. - Draw the title of the current data that is different from the previous data title.
- Repeat 1 and 2.
2. Interface part
public class SpecialGridActivity extends AppCompatActivity {
// GridItem实现了IGridItem接口
private List<GridItem> values;
private RecyclerView mRecyclerView;
private GridItemDecoration itemDecoration;
// 自己封装的RecyclerAdapter
private RecyclerAdapter<GridItem> mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_special_grid);
initWidget();
}
private void initWidget() {
mRecyclerView = findViewById(R.id.rv_content);
// 创建GridLayoutManager,并设置SpanSizeLookup
GridLayoutManager gll = new GridLayoutManager(this, 3);
gll.setSpanSizeLookup(new SpecialSpanSizeLookup());
mRecyclerView.setLayoutManager(gll);
values = initData();
// 自己封装的RecyclerAdapter
mRecyclerView.setAdapter(mAdapter = new RecyclerAdapter<GridItem>(values,null) {
@Override
public ViewHolder<GridItem> onCreateViewHolder(View root, int viewType) {
switch (viewType) {
case R.layout.small_grid_recycle_item:
return new SmallHolder(root);
case R.layout.normal_grid_recycle_item:
return new NormalHolder(root);
case R.layout.special_grid_recycle_item:
return new SpecialHolder(root);
default:
return null;
}
}
@Override
public int getItemLayout(GridItem gridItem, int position) {
switch (gridItem.getType()) {
case GridItem.TYPE_SMALL:
return R.layout.small_grid_recycle_item;
case GridItem.TYPE_NORMAL:
return R.layout.normal_grid_recycle_item;
case GridItem.TYPE_SPECIAL:
return R.layout.special_grid_recycle_item;
}
return 0;
}
});
//...
// 分隔线生成
// 之前的GridItemDecoration代码中我将构建者模式部分省略了
itemDecoration = new GridItemDecoration.Builder(this,values, 3)
.setTitleTextColor(Color.parseColor("#4e5864"))
.setTitleFontSize(22)
.setTitleHeight(52)
.build();
mRecyclerView.addItemDecoration(itemDecoration);
}
// 数据初始化
private List<GridItem> initData() {
List<GridItem> values = new ArrayList<>();
values.add(new GridItem("我很忙", "", R.drawable.head_1,"最近常听",1,GridItem.TYPE_SMALL));
values.add(new GridItem("治愈:有些歌比闺蜜更懂你", "", R.drawable.head_2,"最近常听",1,GridItem.TYPE_SMALL));
values.add(new GridItem("「华语」90后的青春纪念手册", "", R.drawable.head_3,"最近常听",1,GridItem.TYPE_SMALL));
values.add(new GridItem("流行创作女神你霉,泰勒斯威夫特的创作历程", "", R.drawable.special_2
,"更多为你推荐",3,GridItem.TYPE_SPECIAL));
values.add(new GridItem("行走的CD写给别人的歌", "给「跟我走吧」几分,试试这些", R.drawable.normal_1
,"更多为你推荐",3,GridItem.TYPE_NORMAL));
values.add(new GridItem("爱情里的酸甜苦辣,让人捉摸不透", "听完「靠近一点点」,他们等你翻牌", R.drawable.normal_2
,"更多为你推荐",3,GridItem.TYPE_NORMAL));
values.add(new GridItem("关于喜欢你这件事,我都写在了歌里", "「好想你」听罢,听它们吧", R.drawable.normal_3
,"更多为你推荐",3,GridItem.TYPE_NORMAL));
values.add(new GridItem("周杰伦暖心混剪,短短几分钟是多少人的青春", "", R.drawable.special_1
,"更多为你推荐",3,GridItem.TYPE_SPECIAL));
values.add(new GridItem("我好想和你一起听雨滴", "给「发如雪」几分,那这些呢", R.drawable.normal_4
,"更多为你推荐",3,GridItem.TYPE_NORMAL));
values.add(new GridItem("油管周杰伦热门单曲Top20", "「周杰伦」的这些哥,你听了吗", R.drawable.normal_5
,"更多为你推荐",3,GridItem.TYPE_NORMAL));
return values;
}
class SpecialSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int i) {
// 返回在数据中定义的SpanSize
GridItem gridItem = values.get(i);
return gridItem.getSpanSize();
}
}
class SmallHolder extends RecyclerAdapter.ViewHolder<GridItem> {
//... 代码省略,就是设置图片和文字的操作
// 小的Holder
}
class NormalHolder extends RecyclerAdapter.ViewHolder<GridItem> {
//... 中等的Holder
}
class SpecialHolder extends RecyclerAdapter.ViewHolder<GridItem> {
//... 横向大的Holder
}
}
We usually use GridLayoutManager
is not the same, GridLayoutManager
you need to set SpanSizeLookUp
is that we need to set for each child view SpanSize
, because we have achieved each data IGridItem
interface, which will be provided from outside SpanSize
, so we return here in the data set SpanSize
can be .
Due to space limitations, the layout file and package of ReyclerAdapter will not be posted, and interested students can check the source code. The following is what we accomplished:
effect
Four, summary
to sum up
Some details in the source code are very interesting. It is because of GridLayoutManager
the source code that I have read that this article appeared. After reading this article, I believe you, like me, have RecyclerView
a deeper understanding.
Demo site: https://github.com/mCyp/Orient-Ui
If you want and RecyclerView
have a more in-depth exchanges, Welcome to my unraveling the RecyclerView
series of articles :
First: "unraveling RecyclerView - LayoutManager"
The second: "unraveling RecyclerView - piecemeal"
Part III: "unraveling RecyclerView - ItemAnimator & Adapter"
Author: Nine heart _
link: https: //www.jianshu.com/p/60aa2fc17870
Source: Jane books
are copyrighted by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.