阅读 12350
GridLayoutManager这么用,你可能还真没尝试过

GridLayoutManager这么用,你可能还真没尝试过

技术不止,文章有料,关注公众号 九心说,每周一篇高质好文,和九心在大厂路上肩并肩。

前言

上周我在《抽丝剥茧RecyclerView - LayoutManager》一文中提到利用GridLayoutManager可以实现一个如下左边的首页:

图片图片
上期分享效果
有同学对此表示很感兴趣,奈何没有现成的案例,于是自己就简单实现了一个,最终效果是上表中右侧的图。

相信很多同学都和我有一样的感觉,认为GridLayoutManager只能实现标准的网格布局,直到我前段时间决定研究RecyclerView,看了GridLayoutManager的源码,才发现,原来它可以做更多的事,比如说,写一个首页。

阅读本文之前,你需要的一些知识储备

  1. View的绘制流程有一些简单的了解。
  2. Canvas的简单实用。
  3. RecyclerView+GridLayoutManager的使用。

目录

目录

一、场景

使用RecyclerView+GridLayoutManager+ItemDecoration定制首页适用的场景:

  • 有多个功能模块
  • 子视图多个样式
  • 最后一个模块需要刷新(如果有这样的功能,肯定也是通过RecyclerView实现的),例如QQ音乐中往下滑推荐用户可能感兴趣的音乐。

个人觉得该方案的意义在于减少布局的嵌套,让界面管理变得更加简单,但是对于业务特别复杂的情况下可能会不适用。

二、思路

实现以上功能需要解决两个难点

  1. 如何给不同行展示不同数量的子视图
  2. 每个模块标题的绘制

这两个问题的解决方案分别对应着GridLayoutManagerItemDecoration,我们挨个了解。

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是一个很有意思的东西,因为它可以实现一些好玩的东西,比如以下的通讯录的字母标题时间轴

通讯录的字母标题时间轴
通讯录字母标题时间轴
还可以利用它做一些特殊的效果,例如字母标题的吸顶,这里我分别推荐两个库:

这里简单的介绍一下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 < 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);
		}
	}
}
复制代码

}

总的逻辑就是:
1. 如果所处的`RecyclerView`子视图的位置处在标题的下方,那么就需要预留空间,设置在`outRect`中,需要注意的是,同一行的多个子视图都需要预留空间。
2. 对不同于上一个数据标题的当前数据进行标题的绘制。
3. 重复执行1、2。
#### 2. 界面部分
复制代码

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
}
复制代码

}

与我们平时使用`GridLayoutManager`不一样的是,`GridLayoutManager`需要设置`SpanSizeLookUp`,就是需要我们给每个子视图的设置`SpanSize`,因为我们每个数据都实现了`IGridItem`接口,该接口会向外提供`SpanSize`,所以这里返回我们在数据中设置的`SpanSize`即可。

限于篇幅,布局文件以及ReyclerAdapter的封装就不贴了,感兴趣的同学可以查看一下源代码。以下就是我们完成的效果:
![效果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/91972d080aec49379e138ec6bade3cd4~tplv-k3u1fbpfcp-zoom-1.image)
## 四、总结
![总结](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb638a2968914da5ac6053b58c6492e3~tplv-k3u1fbpfcp-zoom-1.image)
源码中的一些细节是很有趣的,正是因为阅读了`GridLayoutManager`的源码,才有了本文的出现。读完本文之后,相信你和我一样,对`RecyclerView`有了更深的了解。

Demo地址:[https://github.com/mCyp/Orient-Ui](https://github.com/mCyp/Orient-Ui)

如果你希望和`RecyclerView`有着更深入的交流,欢迎阅读我的**抽丝剥茧`RecyclerView`系列文章**:
> 第一篇:[《抽丝剥茧RecyclerView - LayoutManager》](https://www.jianshu.com/p/f2a5764a370b)  
第二篇:[《抽丝剥茧RecyclerView - 化整为零》](https://www.jianshu.com/p/1ae2f2fcff2c)
复制代码
文章分类
Android
文章标签