(给
安卓开发精选
加星标)
转自:TeaOf
https://juejin.im/post/5d6c871b518825168d37ceb9
前言
上周我在
《抽丝剥茧RecyclerView - LayoutManager》一文中提到利用GridLayoutManager可以实现一个如下左边的首页:
有同学对此表示很感兴趣,奈何没有现成的案例,于是自己就简单实现了一个,最终效果是上表中右侧的图。
相信很多同学都和我有一样的感觉,认为GridLayoutManager只能实现标准的网格布局,直到我前段时间决定研究RecyclerView,看了GridLayoutManager的源码,才发现,原来它可以做更多的事,比如说,写一个首页。
阅读本文之前,你需要的一些知识储备:
-
对View的绘制流程有一些简单的了解。
-
Canvas的简单实用。
-
RecyclerView+GridLayoutManager的使用。
目录
使用RecyclerView+GridLayoutManager+ItemDecoration定制首页适用的场景:
个人觉得该方案的意义在于减少布局的嵌套,让界面管理变得更加简单,但是对于业务特别复杂的情况下可能会不适用。
实现以上功能需要解决两个难点:
-
如何给不同行展示不同数量的子视图
-
每个模块标题的绘制
这两个问题的解决方案分别对应着GridLayoutManager和ItemDecoration,我们挨个了解。
1. GridLayoutManager
GridLayoutManager其实我们已经很熟悉了,只是我们平时没有了解SpanSize这个概念,先看如下一段代码:
GridLayoutManager gll = new
GridLayoutManager(this, 6);
mRecyclerView.setLayoutManager(gll);
上面的代码中我们创建了一个纵向、每行最多容量6个子View的GridLayoutManager,默认情况下,一行总的SpanSize为6,每个子视图默认的SpanSize为1,所以不做处理的情况下GridLayoutManager会将每一行分成6份,每一份展示一个子视图,如下图的第一行:
这时,我如果将子视图的SpanSize都设置为2,那么这个子视图将占整个RecyclerView可用宽度的2/6,如上图第二行,同理,我将SpanSize上升为3,那么该子视图的宽度也就上升为可用的宽度的3/6,如上图第三行,这也是GridLayoutManager能够在不同行设置不同数量的子视图的原因,当然了,你也可以将同一行里面的三个子视图SpanSize分别设置为1、2、3。
好了,距离代码实战还差一个如何绘制标题。
2. ItemDecoration
分割线ItemDecoration是一个很有意思的东西,因为它可以实现一些好玩的东西,比如以下的通讯录的字母标题和时间轴:
还可以利用它做一些特殊的效果,例如字母标题的吸顶,这里我分别推荐两个库:
vivian的时间轴TimeLine
https://github.com/vivian8725118/TimeLine
mcxtzhang的通讯录标题,可实现吸顶SuspensionIndexBar
https://github.com/mcxtzhang/SuspensionIndexBar
这里简单的介绍一下ItemDecoration的原理,这里我就默认同学们已经了解View的测绘流程,主要分为两部分:
1. 将分隔线绘制在RecyclerView子视图的下层,因为分隔线ItemDecoration第一个绘制方法ItemDecoration#onDraw发生在绘制RecyclerView子视图之前,如果你想让其显示出来,需要给ItemDecoration设置偏移量,让子视图偏移,从而不会遮挡ItemDecoration。
2. 将分隔线绘制在RecyclerView子视图的上层,因为其绘制方法ItemDecoration#onDrawOver发生在RecyclerView子视图绘制绘制完成以后,这也是ItemDecoration能够实现吸顶的效果。
有了上面的知识储备,下面就简单了。
1. 自定义ItemDecoration
自定义ItemDecoration需要实现的三个方法,跟我们上面提及的原理相关:
方法名
|
解释
|
onDraw
|
绘制子视图下层的分隔线
|
getItemOffsets
|
通常为了显示下层分隔线而预留的空间
|
onDrawOver
|
绘制上层的分隔线
|
我们的任务仅仅是绘制一个标题,所以使用上面的两个方法就够了。
1.1 定义数据接口
/**
* 数据约束
*/
public interface IGridItem {
/**
* 是否启用分割线
* @return true
*/
boolean isShow();
/**
* 分类标签
*/
String getTag();
/**
* 权重
*/
int getSpanSize();
}
1.2 自定义ItemDecoration类
核心代码就100多行:
/**
* 适用于GridLayoutManager的分割线
*/
public class GridItemDecoration extends RecyclerView.ItemDecoration {
// 记录上次偏移位置 防止一行多个数据的时候视图偏移
private List 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 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);
}
}
}
}
总的逻辑就是:
1. 如果所处的RecyclerView子视图的位置处在标题的下方,那么就需要预留空间,设置在outRect中,需要注意的是,同一行的多个子视图都需要预留空间。
2. 对不同于上一个数据标题的当前数据进行标题的绘制。
3. 重复执行1、2。
2. 界面部分
public class SpecialGridActivity extends AppCompatActivity {
// GridItem实现了IGridItem接口
private List values;
private RecyclerView mRecyclerView;
private GridItemDecoration itemDecoration;
// 自己封装的RecyclerAdapter
private RecyclerAdapter 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(values,null) {
@Override
public ViewHolder 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