上週我在《抽絲剝繭RecyclerView - LayoutManager》一文中提到利用GridLayoutManager
能夠實現一個以下左邊的首頁:git
圖片 | 圖片 |
---|---|
有同窗對此表示很感興趣,奈何沒有現成的案例,因而本身就簡單實現了一個,最終效果是上表中右側的圖。github
相信不少同窗都和我有同樣的感受,認爲GridLayoutManager
只能實現標準的網格佈局,直到我前段時間決定研究RecyclerView
,看了GridLayoutManager
的源碼,才發現,原來它能夠作更多的事,好比說,寫一個首頁。canvas
閱讀本文以前,你須要的一些知識儲備:bash
View
的繪製流程有一些簡單的瞭解。Canvas
的簡單實用。RecyclerView+GridLayoutManager
的使用。使用RecyclerView
+GridLayoutManager
+ItemDecoration
定製首頁適用的場景:ide
RecyclerView
實現的),例如QQ音樂中往下滑推薦用戶可能感興趣的音樂。我的以爲該方案的意義在於減小布局的嵌套,讓界面管理變得更加簡單,可是對於業務特別複雜的狀況下可能會不適用。佈局
實現以上功能須要解決兩個難點:post
這兩個問題的解決方案分別對應着GridLayoutManager
和ItemDecoration
,咱們挨個瞭解。ui
GridLayoutManager
其實咱們已經很熟悉了,只是咱們平時沒有了解SpanSize
這個概念,先看以下一段代碼:this
GridLayoutManager gll = new GridLayoutManager(this, 6);
mRecyclerView.setLayoutManager(gll);
複製代碼
上面的代碼中咱們建立了一個縱向、每行最多容量6個子View的GridLayoutManager
,默認狀況下,一行總的SpanSize
爲6,每一個子視圖默認的SpanSize
爲1,因此不作處理的狀況下GridLayoutManager
會將每一行分紅6份,每一份展現一個子視圖,以下圖的第一行: spa
SpanSize
都設置爲2,那麼這個子視圖將佔整個RecyclerView可用寬度的
2/6
,如上圖第二行,同理,我將
SpanSize
上升爲3,那麼該子視圖的寬度也就上升爲可用的寬度的
3/6
,如上圖第三行,這也是
GridLayoutManager
可以在不一樣行設置不一樣數量的子視圖的緣由,固然了,你也能夠將同一行裏面的三個子視圖
SpanSize
分別設置爲一、二、3。
好了,距離代碼實戰還差一個如何繪製標題。
分割線ItemDecoration
是一個頗有意思的東西,由於它能夠實現一些好玩的東西,好比如下的通信錄的字母標題和時間軸:
通信錄的字母標題 | 時間軸 |
---|---|
還能夠利用它作一些特殊的效果,例如字母標題的吸頂,這裏我分別推薦兩個庫:
這裏簡單的介紹一下ItemDecoration
的原理,這裏我就默認同窗們已經瞭解View
的測繪流程,主要分爲兩部分:
RecyclerView
子視圖的下層,由於分隔線ItemDecoration
第一個繪製方法ItemDecoration#onDraw
發生在繪製RecyclerView
子視圖以前,若是你想讓其顯示出來,須要給ItemDecoration
設置偏移量,讓子視圖偏移,從而不會遮擋ItemDecoration
。RecyclerView
子視圖的上層,由於其繪製方法ItemDecoration#onDrawOver
發生在RecyclerView
子視圖繪製繪製完成之後,這也是ItemDecoration
可以實現吸頂的效果。有了上面的知識儲備,下面就簡單了。
自定義ItemDecoration
須要實現的三個方法,跟咱們上面說起的原理相關:
方法名 | 解釋 |
---|---|
onDraw |
繪製子視圖下層的分隔線 |
getItemOffsets |
一般爲了顯示下層分隔線而預留的空間 |
onDrawOver |
繪製上層的分隔線 |
咱們的任務僅僅是繪製一個標題,因此使用上面的兩個方法就夠了。
/**
* 數據約束
*/
public interface IGridItem {
/**
* 是否啓用分割線
* @return true
*/
boolean isShow();
/**
* 分類標籤
*/
String getTag();
/**
* 權重
*/
int getSpanSize();
}
複製代碼
核心代碼就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);
}
}
}
}
複製代碼
總的邏輯就是:
RecyclerView
子視圖的位置處在標題的下方,那麼就須要預留空間,設置在outRect
中,須要注意的是,同一行的多個子視圖都須要預留空間。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的封裝就不貼了,感興趣的同窗能夠查看一下源代碼。如下就是咱們完成的效果:
GridLayoutManager
的源碼,纔有了本文的出現。讀完本文以後,相信你和我同樣,對
RecyclerView
有了更深的瞭解。
Demo地址:github.com/mCyp/Orient…
若是你但願和RecyclerView
有着更深刻的交流,歡迎閱讀個人抽絲剝繭RecyclerView
系列文章:
第一篇:《抽絲剝繭RecyclerView - LayoutManager》
第二篇:《抽絲剝繭RecyclerView - 化整爲零》