写点什么

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

发布于: 2021 年 11 月 07 日

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


  1. 对 View 的绘制流程有一些简单的了解。

  2. Canvas 的简单实用。

  3. RecyclerView+GridLayoutManager 的使用。

目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r0aANdR2-1571057729479)(https://upload-images.jianshu.io/upload_images/15679108-8e15e077e709ab1d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

一、场景

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


  • 有多个功能模块

  • 子视图多个样式

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


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

二、思路

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


  1. 如何给不同行展示不同数量的子视图

  2. 每个模块标题的绘制


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

1. GridLayoutManager

GridLayoutManager 其实我们已经很熟悉了,只是我们平时没有了解 SpanSize 这个概念,先看如下一段代码:


GridLayoutManager?gll?=?new?GridLayoutManager(this,?6);


mRecyclerView.setLayoutManager(gll);


上面的代码中我们创建了一个纵向、每行最多容量 6 个子 View 的 GridLayoutManager,默认情况下,一行总的 SpanSize 为 6,每个子视图默认的 SpanSize 为 1,所以不做处理的情况下 GridLayoutManager 会将每一行分成 6 份,每一份展示一个子视图,如下图的第一行:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MoM2W1h6-1571057729481)(https://upload-images.jianshu.io/upload_images/15679108-bdff703c1ff803ff?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]


这时,我如果将子视图的 SpanSize 都设置为 2,那么这个子视图将占整个 RecyclerView 可用宽度的 2/6,如上图第二行,同理,我将 SpanSize 上升为 3,那么该子视图的宽度也就上升为可用的宽度的 3/6,如上图第三行,这也是 GridLayoutManager 能够在不同行设置不同数量的子视图的原因,当然了,你也可以将同一行里面的三个子视图 SpanSize 分别设置为 1、2、3。好了,距离代码实战还差一个如何绘制标题。

2. ItemDecoration

分割线 ItemDecoration 是一个很有意思的东西,因为它可以实现一些好玩的东西,比如以下的通讯录的字母标题和时间轴:


| 通讯录的字母标题 | 时间轴 |


| --- | --- |


| [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GbVdIX8H-1571057729482)(https://upload-images.jianshu.io/upload_images/15679108-f8175304e0293a7d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)] | |


| [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OY7TGnMk-1571057729482)(https://upload-images.jianshu.io/upload_images/15679108-54660275e4b3e98e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]


|


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


  • vivian 的时间轴 TimeLine

  • 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<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);


    }


    }


    }


    }


    总的逻辑就是:


    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这么用,你可能还真没尝试过