1.概述
recyclerView的要實(shí)現(xiàn)分割線要繼承RecyclerView.ItemDecoration
抽象類(lèi)似扔。谷歌官方為我們提供實(shí)例DividerItemDecoration
吨些。ItmDecoration有三個(gè)重要的方法,如果只是想要設(shè)置分割線搓谆,只要getItemOffsets
就可以炒辉。
方法 | 作用 |
---|---|
getItemOffsets | 設(shè)置item上下左右的距離 |
onDraw | 可以通過(guò)它繪制分割線的顏色圖片等,但它的區(qū)域超過(guò)item時(shí)會(huì)被item覆蓋 |
onDrawOver | 同樣可以通過(guò)它繪制顏色和圖片等泉手,但它的區(qū)域會(huì)覆蓋item |
看看源碼的注釋?zhuān)?/p>
public abstract static class ItemDecoration {
/**
*將任何適當(dāng)?shù)难b飾畫(huà)到提供給RecyclerView的Canvas中黔寇。
*用這種方法繪制的任何內(nèi)容都將在繪制項(xiàng)目視圖之前繪制,因此將出現(xiàn)在視圖下
*/
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
/**
*將任何適當(dāng)?shù)难b飾畫(huà)到提供給RecyclerView的Canvas中斩萌。
* 用這種方法繪制的任何內(nèi)容都將在繪制項(xiàng)目視圖之后繪制缝裤,并因此出現(xiàn)在視圖上方。
*/
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
onDrawOver(c, parent);
}
/**
*檢索給定項(xiàng)目的所有偏移量颊郎。
* outRect的每個(gè)字段指定插入項(xiàng)目視圖的像素?cái)?shù)憋飞,類(lèi)似于填充或邊距。
*默認(rèn)實(shí)現(xiàn)將outRect的邊界設(shè)置為0并返回姆吭。
*
* <p>
*如果需要訪問(wèn)適配器以獲取其他數(shù)據(jù)榛做,
*則可以調(diào)用{@link RecyclerView#getChildAdapterPosition(View)}
*以獲取視圖的適配器位置。
*/
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
2.自定義ItemDecoration
自定一個(gè)ItemDecoration來(lái)實(shí)現(xiàn)一個(gè)可以吸頂效果的列表内狸,看看效果圖:
要實(shí)現(xiàn)這個(gè)效果检眯,首先用onDraw
繪制一個(gè)紅色背景的分割線,第二就是用onDrawOver
繪制吸頂?shù)募t色頭部昆淡。
public class RecyclerViewDecoration extends RecyclerView.ItemDecoration {
private int groupHeaderHeight;
private Paint headPaint;
private Paint textPaint;
private Rect textRect;
public StarDecoration(Context context) {
groupHeaderHeight = dp2px(context, 100);
//畫(huà)吸頂?shù)漠?huà)筆
headPaint = new Paint();
headPaint.setColor(Color.RED);
// 畫(huà)文字的畫(huà)筆
textPaint = new Paint();
textPaint.setTextSize(50);
textPaint.setColor(Color.WHITE);
textRect = new Rect();
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if (parent.getAdapter() instanceof StarAdapter) {
StarAdapter adapter = (StarAdapter) parent.getAdapter();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int count = parent.getChildCount(); // 當(dāng)前屏幕的item個(gè)數(shù)
for (int i = 0; i < count; i++) {
// 獲取對(duì)應(yīng)i的View
View view = parent.getChildAt(i);
// 獲取View的布局位置
int position = parent.getChildLayoutPosition(view);
// 是否繪制紅色的頭部分割線
boolean isGroupHeader = adapter.isGourpHeader(position);
if (isGroupHeader && view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
//在view的頂部畫(huà)紅色頭部分割線
c.drawRect(left, view.getTop() - groupHeaderHeight, right, view.getTop(), headPaint);
//繪制文字
String groupName = adapter.getGroupName(position);
//得到文字需要的rect
textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
c.drawText(groupName, left + 20, view.getTop() -
groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
} else if (view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
// 繪制紅色分割線
c.drawRect(left, view.getTop() - 4, right, view.getTop(), headPaint);
}
}
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter() instanceof StarAdapter) {
StarAdapter adapter = (StarAdapter) parent.getAdapter();
// 返回可見(jiàn)區(qū)域內(nèi)的第一個(gè)item的position
int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
// 獲取對(duì)應(yīng)position的View
View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int top = parent.getPaddingTop();
// 當(dāng)?shù)诙€(gè)是組的頭部的時(shí)候
boolean isGroupHeader = adapter.isGourpHeader(position + 1);
//下一個(gè)紅色吸頂頭部位于第一個(gè)位置
if (isGroupHeader) {
int bottom = Math.min(groupHeaderHeight, itemView.getBottom() - parent.getPaddingTop());
c.drawRect(left, top, right, top + bottom, headPaint);//上一個(gè)不斷縮短
String groupName = adapter.getGroupName(position);
c.clipRect(left, top, right, top + bottom);
textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
c.drawText(groupName, left + 20, top + bottom
- groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
} else {
//繪制一個(gè)吸頂?shù)募t色頭部
c.drawRect(left, top, right, top + groupHeaderHeight, headPaint);
//繪制文字
String groupName = adapter.getGroupName(position);
textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
c.drawText(groupName, left + 20, top + groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
}
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (parent.getAdapter() instanceof StarAdapter) {
StarAdapter adapter = (StarAdapter) parent.getAdapter();
int position = parent.getChildLayoutPosition(view);
boolean isGroupHeader = adapter.isGourpHeader(position);
// 怎么判斷 itemView是頭部
if (isGroupHeader) {
// 如果是頭部锰瘸,預(yù)留更大的地方
outRect.set(0, groupHeaderHeight, 0, 0);
} else {
// 1像素
outRect.set(0, 4, 0, 0);
}
}
}
private int dp2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale * 0.5f);
}
}
通過(guò)代碼可以分為三步
- 通過(guò)
getItemOffsets
設(shè)置Item大和小的分割線- 在
onDraw
中為每個(gè)大小分割線繪制成紅色,有文字的繪制文字- 在
onDrawOver
繪制一個(gè)吸頂紅色頭部昂灵,然后在下一個(gè)紅色頭部的前一個(gè)item變成第一個(gè)可見(jiàn)時(shí)避凝,吸頂紅色頭部在[100,0]區(qū)間不斷的縮小舞萄。
3.ItemDecoration的原理
要搞懂ItemDecoration的原理,主要有兩方面:
1.
ItemDecoration
是怎樣實(shí)現(xiàn)ItemView
之間的間距的
2.ItemDecoration
是如何繪制的
3.1 ItemDecoration如何實(shí)現(xiàn)間距
要理解ItemDecoration的間距管削,還要從RecyclerView
的測(cè)量和ItemView
的布局開(kāi)始鹏氧。測(cè)量主要是通過(guò)LayoutManager
的measureChildWithMargins
方法
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//拿到全部分割線的左右上下全部總和
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
//將需要的分割線總和大小加到寬高中
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//確定總寬高
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
這里有個(gè)getItemDecorInsetsForChild
方法,總要是拿到全部分割線上下左右全部相加的距離佩谣。 第二步是把还,把分割線的寬高總大小和ItemView寬高總大小進(jìn)行相加
,并跟可滑動(dòng)的方向確定RecyclerView的寬高茸俭。所以吊履,全部分割線所需要寬高已經(jīng)加到RecyclerView的寬高中
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
final boolean positionWillChange =
(lp.mViewHolder.getLayoutPosition() != lp.mViewHolder.getAbsoluteAdapterPosition());
if (mState.isPreLayout()
&& (lp.isItemChanged() || lp.isViewInvalid() || positionWillChange)) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
接下來(lái),看看ItemView的布局调鬓。代碼主要在LayoutManager
的layoutDecoratedWithMargins
中
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//拿到View的分割線大小
final Rect insets = lp.mDecorInsets;
//item布局時(shí)會(huì)在上下左右留出分割線的所需的大小
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
代碼很簡(jiǎn)單艇炎,itemview在布局的時(shí)候,就已經(jīng)留出分割線的大小了腾窝。所以我們的分割線的并不是onDraw
onDrawOver
繪制出來(lái)的缀踪,而是在ItemView
布局時(shí)就根據(jù)getItemOffsets
方法設(shè)置上下左右距離開(kāi)始留出間距。所以我們自定義分割線時(shí)虹脯,只寫(xiě)getItemOffsets
方法也可以繪制分割線效果驴娃。
3.2 ItemDecoration的繪制
RecyclerView
的draw
繪制流程,主要就是繪制ItemDecoration
的onDraw
onDrawOver
,和繪制itemView
循集。
@Override
public void draw(Canvas c) {
//通過(guò)調(diào)用onDraw繪制item和ItemDecoration的onDraw()
super.draw(c);
//繪制ItemDecoration的onDrawOver()
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
//padding滑動(dòng)的處理
// TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
// need find children closest to edges. Not sure if it is worth the effort.
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
代碼比較簡(jiǎn)單唇敞,不作分析。