前言
上周我在《抽絲剝繭RecyclerView - LayoutManager》一文中提到利用GridLayoutManager
可以實(shí)現(xiàn)一個(gè)如下的首頁:
有同學(xué)對(duì)此表示很感興趣,奈何沒有現(xiàn)成的案例毅否,于是自己就簡(jiǎn)單實(shí)現(xiàn)了一個(gè)矮固,最終效果如下:
相信很多同學(xué)都和我有一樣的感覺,認(rèn)為
GridLayoutManager
只能實(shí)現(xiàn)標(biāo)準(zhǔn)的網(wǎng)格布局氯质,直到我前段時(shí)間決定研究RecyclerView
募舟,看了GridLayoutManager
的源碼,才發(fā)現(xiàn)闻察,原來它可以做更多的事拱礁,比如說,寫一個(gè)首頁辕漂。
閱讀本文之前呢灶,你需要的一些知識(shí)儲(chǔ)備:
- 對(duì)
View
的繪制流程有一些簡(jiǎn)單的了解。 -
Canvas
的簡(jiǎn)單實(shí)用钉嘹。 -
RecyclerView+GridLayoutManager
的使用鸯乃。
目錄
一、場(chǎng)景
使用RecyclerView
+GridLayoutManager
+ItemDecoration
定制首頁適用的場(chǎng)景:
- 有多個(gè)功能模塊
- 子視圖多個(gè)樣式
- 最后一個(gè)模塊需要刷新(如果有這樣的功能跋涣,肯定也是通過
RecyclerView
實(shí)現(xiàn)的)缨睡,例如QQ音樂中往下滑推薦用戶可能感興趣的音樂。
個(gè)人覺得該方案的意義在于減少布局的嵌套仆潮,讓界面管理變得更加簡(jiǎn)單宏蛉,但是對(duì)于業(yè)務(wù)特別復(fù)雜的情況下可能會(huì)不適用。
二性置、思路
實(shí)現(xiàn)以上功能需要解決兩個(gè)難點(diǎn):
- 如何給不同行展示不同數(shù)量的子視圖
- 每個(gè)模塊標(biāo)題的繪制
這兩個(gè)問題的解決方案分別對(duì)應(yīng)著GridLayoutManager
和ItemDecoration
拾并,我們挨個(gè)了解。
1. GridLayoutManager
GridLayoutManager
其實(shí)我們已經(jīng)很熟悉了,只是我們平時(shí)沒有了解SpanSize
這個(gè)概念嗅义,先看如下一段代碼:
GridLayoutManager gll = new GridLayoutManager(this, 6);
mRecyclerView.setLayoutManager(gll);
上面的代碼中我們創(chuàng)建了一個(gè)縱向屏歹、每行最多容量6個(gè)子View的GridLayoutManager
,默認(rèn)情況下之碗,一行總的SpanSize
為6蝙眶,每個(gè)子視圖默認(rèn)的SpanSize
為1,所以不做處理的情況下GridLayoutManager
會(huì)將每一行分成6份褪那,每一份展示一個(gè)子視圖幽纷,如下圖的第一行:
這時(shí),我如果將子視圖的
SpanSize
都設(shè)置為2博敬,那么這個(gè)子視圖將占整個(gè)RecyclerView可用寬度的2/6
友浸,如上圖第二行,同理偏窝,我將SpanSize
上升為3收恢,那么該子視圖的寬度也就上升為可用的寬度的3/6
,如上圖第三行祭往,這也是GridLayoutManager
能夠在不同行設(shè)置不同數(shù)量的子視圖的原因伦意,當(dāng)然了,你也可以將同一行里面的三個(gè)子視圖SpanSize
分別設(shè)置為1硼补、2驮肉、3。
好了括勺,距離代碼實(shí)戰(zhàn)還差一個(gè)如何繪制標(biāo)題缆八。
2. ItemDecoration
分割線ItemDecoration
是一個(gè)很有意思的東西,因?yàn)樗梢詫?shí)現(xiàn)一些好玩的東西疾捍,比如以下的通訊錄的字母標(biāo)題和時(shí)間軸:
還可以利用它做一些特殊的效果奈辰,例如字母標(biāo)題的吸頂,這里我分別推薦兩個(gè)庫:
- vivian的時(shí)間軸TimeLine
- mcxtzhang的通訊錄標(biāo)題乱豆,可實(shí)現(xiàn)吸頂SuspensionIndexBar
這里簡(jiǎn)單的介紹一下ItemDecoration
的原理奖恰,這里我就默認(rèn)同學(xué)們已經(jīng)了解View
的測(cè)繪流程,主要分為兩部分:
- 將分隔線繪制在
RecyclerView
子視圖的下層宛裕,因?yàn)榉指艟€ItemDecoration
第一個(gè)繪制方法ItemDecoration#onDraw
發(fā)生在繪制RecyclerView
子視圖之前瑟啃,如果你想讓其顯示出來,需要給ItemDecoration
設(shè)置偏移量揩尸,讓子視圖偏移蛹屿,從而不會(huì)遮擋ItemDecoration
。 - 將分隔線繪制在
RecyclerView
子視圖的上層岩榆,因?yàn)槠淅L制方法ItemDecoration#onDrawOver
發(fā)生在RecyclerView
子視圖繪制繪制完成以后错负,這也是ItemDecoration
能夠?qū)崿F(xiàn)吸頂?shù)男Ч?/li>
三坟瓢、代碼實(shí)戰(zhàn)
有了上面的知識(shí)儲(chǔ)備,下面就簡(jiǎn)單了犹撒。
1. 自定義ItemDecoration
自定義ItemDecoration
需要實(shí)現(xiàn)的三個(gè)方法折联,跟我們上面提及的原理相關(guān):
方法名 | 解釋 |
---|---|
onDraw |
繪制子視圖下層的分隔線 |
getItemOffsets |
通常為了顯示下層分隔線而預(yù)留的空間 |
onDrawOver |
繪制上層的分隔線 |
我們的任務(wù)僅僅是繪制一個(gè)標(biāo)題,所以使用上面的兩個(gè)方法就夠了识颊。
1.1 定義數(shù)據(jù)接口
/**
* 數(shù)據(jù)約束
*/
public interface IGridItem {
/**
* 是否啟用分割線
* @return true
*/
boolean isShow();
/**
* 分類標(biāo)簽
*/
String getTag();
/**
* 權(quán)重
*/
int getSpanSize();
}
1.2 自定義ItemDecoration類
核心代碼就100多行:
/**
* 適用于GridLayoutManager的分割線
*/
public class GridItemDecoration extends RecyclerView.ItemDecoration {
// 記錄上次偏移位置 防止一行多個(gè)數(shù)據(jù)的時(shí)候視圖偏移
private List<Integer> offsetPositions = new ArrayList<>();
// 顯示數(shù)據(jù)
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);
// 繪制標(biāo)題的邏輯:
// 如果該行的數(shù)據(jù)的需要顯示的標(biāo)題不同于上行的標(biāo)題诚镰,就繪制標(biāo)題
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);
}
}
}
}
/**
* 繪制標(biāo)題
*
* @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);
// 預(yù)留邏輯:
// 只要是標(biāo)題下面的一行,無論這行幾個(gè)祥款,都要預(yù)留空間給標(biāo)題顯示
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);
}
}
}
}
總的邏輯就是:
- 如果所處的
RecyclerView
子視圖的位置處在標(biāo)題的下方清笨,那么就需要預(yù)留空間,設(shè)置在outRect
中刃跛,需要注意的是函筋,同一行的多個(gè)子視圖都需要預(yù)留空間。 - 對(duì)不同于上一個(gè)數(shù)據(jù)標(biāo)題的當(dāng)前數(shù)據(jù)進(jìn)行標(biāo)題的繪制奠伪。
- 重復(fù)執(zhí)行1、2首懈。
2. 界面部分
public class SpecialGridActivity extends AppCompatActivity {
// GridItem實(shí)現(xiàn)了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);
// 創(chuàng)建GridLayoutManager绊率,并設(shè)置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代碼中我將構(gòu)建者模式部分省略了
itemDecoration = new GridItemDecoration.Builder(this,values, 3)
.setTitleTextColor(Color.parseColor("#4e5864"))
.setTitleFontSize(22)
.setTitleHeight(52)
.build();
mRecyclerView.addItemDecoration(itemDecoration);
}
// 數(shù)據(jù)初始化
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后的青春紀(jì)念手冊(cè)", "", R.drawable.head_3,"最近常聽",1,GridItem.TYPE_SMALL));
values.add(new GridItem("流行創(chuàng)作女神你霉,泰勒斯威夫特的創(chuàng)作歷程", "", 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("愛情里的酸甜苦辣滤否,讓人捉摸不透", "聽完「靠近一點(diǎn)點(diǎn)」,他們等你翻牌", R.drawable.normal_2
,"更多為你推薦",3,GridItem.TYPE_NORMAL));
values.add(new GridItem("關(guān)于喜歡你這件事最仑,我都寫在了歌里", "「好想你」聽罷藐俺,聽它們吧", R.drawable.normal_3
,"更多為你推薦",3,GridItem.TYPE_NORMAL));
values.add(new GridItem("周杰倫暖心混剪,短短幾分鐘是多少人的青春", "", R.drawable.special_1
,"更多為你推薦",3,GridItem.TYPE_SPECIAL));
values.add(new GridItem("我好想和你一起聽雨滴", "給「發(fā)如雪」幾分泥彤,那這些呢", 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) {
// 返回在數(shù)據(jù)中定義的SpanSize
GridItem gridItem = values.get(i);
return gridItem.getSpanSize();
}
}
class SmallHolder extends RecyclerAdapter.ViewHolder<GridItem> {
//... 代碼省略,就是設(shè)置圖片和文字的操作
// 小的Holder
}
class NormalHolder extends RecyclerAdapter.ViewHolder<GridItem> {
//... 中等的Holder
}
class SpecialHolder extends RecyclerAdapter.ViewHolder<GridItem> {
//... 橫向大的Holder
}
}
與我們平時(shí)使用GridLayoutManager
不一樣的是吟吝,GridLayoutManager
需要設(shè)置SpanSizeLookUp
菱父,就是需要我們給每個(gè)子視圖的設(shè)置SpanSize
,因?yàn)槲覀兠總€(gè)數(shù)據(jù)都實(shí)現(xiàn)了IGridItem
接口剑逃,該接口會(huì)向外提供SpanSize
浙宜,所以這里返回我們?cè)跀?shù)據(jù)中設(shè)置的SpanSize
即可。
限于篇幅蛹磺,布局文件以及ReyclerAdapter的封裝就不貼了粟瞬,感興趣的同學(xué)可以查看一下源代碼。以下就是我們完成的效果:
四萤捆、總結(jié)
源碼中的一些細(xì)節(jié)是很有趣的裙品,正是因?yàn)殚喿x了
GridLayoutManager
的源碼俗批,才有了本文的出現(xiàn)。讀完本文之后清酥,相信你和我一樣扶镀,對(duì)RecyclerView
有了更深的了解。
Demo地址:https://github.com/mCyp/Orient-Ui
如果你希望和RecyclerView
有著更深入的交流焰轻,歡迎閱讀我的抽絲剝繭RecyclerView
系列文章:
第一篇:《抽絲剝繭RecyclerView - LayoutManager》
第二篇:《抽絲剝繭RecyclerView - 化整為零》
第三篇:《抽絲剝繭RecyclerView - ItemAnimator & Adapter》