該文詳細的介紹了RecyclerView.ItemDecoration實現分組粘性頭部的功能,讓咱們本身生產代碼,告別代碼搬運工的時代.另外文末附有完整Demo的鏈接.看下效果:
json
RecyclerView.ItemDecoration對於咱們最熟悉的功能就是給RecyclerView實現各類各樣自定義的分割線了,實現分割線的功能其實和實現粘性頭部的功能大同小異,那咱們就來看看這神奇的RecyclerView.ItemDecoration.bash
該類是RecyclerView的內部靜態抽象類:ide
public abstract static class ItemDecoration {
/**
* 繪製*除Item內容*之外的佈局,這個方法是再****Item的內容繪製以前****執行的,
* 因此呢若是兩個繪製區域重疊的話,Item的繪製區域會覆蓋掉該方法繪製的區域.
* 通常配合getItemOffsets來繪製分割線等.
*
* @param c Canvas 畫布
* @param parent RecyclerView
* @param state RecyclerView的狀態
*/
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
/**
* 繪製*除Item內容*之外的東西,這個方法是在****Item的內容繪製以後****才執行的,
* 因此該方法繪製的東西會將Item的內容覆蓋住,既顯示在Item之上.
* 通常配合getItemOffsets來繪製分組的頭部等.
*
* @param c Canvas 畫布
* @param parent RecyclerView
* @param state RecyclerView的狀態
*/
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
/**
* @deprecated
* Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
/**
* @deprecated
* Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
/**
* 設置Item的佈局四周的間隔.
*
* @param outRect 肯定間隔 Left Top Right Bottom 數值的矩形.
* @param view RecyclerView的ChildView也就是每一個Item的的佈局.
* @param parent RecyclerView自己.
* @param state RecyclerView的各類狀態.
*/
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
複製代碼
這裏面呢有個問題必定要明白幾個問題:佈局
getItemOffsets這個方法設置的Item間隔究竟是那個間隔?測試
咱們來看一張圖. ui
咱們知道getItemOffsets()第一個參數是一個矩形的對象,這個對象的left、 top、right、bottpm四個屬性值分別表示圖中的outRect.left、outRect.top、outRect.right、outRect.bottom四個線段所表示的空間.也就是說當RecyclerView的Item再肯定本身的大小的時候會將getItemOffsets()裏面的Rect對象的Left、Top、Right、Bottom屬性取出來,看看須要再Item佈局的四周留出多大的空間.咱們來看下源碼:this
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// 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);
//這裏呢mTempRect就是咱們再getItemOffsets()裏面的第一個Rect的對象,咱們再實現類的方法裏面給mTempRect賦值.
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;
}
這裏呢就是RecyclerView再測量每一個Child的大小的時候都把insets這個矩形的l t r b 數值都加上了.insets就是方法getItemDecorInsetsForChild()返回的矩形對象.
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView and any added item decorations into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child Child view to measure
* @param widthUsed Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
public void measureChild(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() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
複製代碼
源碼的講解過於粗糙,但願你們見諒,目的就是爲了讓你們知道這個getItemOffsets()方法是怎麼讓RecyclerView再Item以外留出空間的.spa
onDraw()和onDrawOver()方法應該用哪個?.net
首先咱們看過上面的代碼以後知道,onDraw執行再Item的繪製以前,也就是ItemDecoration的onDraw方法先執行,再執行Item的onDraw方法,這樣Item的內容就會覆蓋在ItemDecoration的onDraw上面.ItemDecoration的onDrawOver()方法執行在Item的繪製以後,那就是onDrawOver()繪製的內容會覆蓋再Item內容之上.這樣就造成了層層遮蓋的問題,那麼咱們日常的分割線一般繪製在ItemDecoration的onDraw()方法裏面,爲了不Item的內容覆蓋掉,咱們就要getItemOffsets()爲咱們留出繪製的空間了.這樣咱們的思路不是不有了呢.3d
咱們能夠用onDrawOver()和getItemOffsets()方法一塊兒使用來實現Item的粘性頭部和頂部懸浮的效果.
咱們要作的是區域分組顯示,每一個分組的開始要有一個粘性頭部.如圖所示:
首前後臺返回的數據必定要有組類區分,每一個分組的標記不能同樣,最好是咱們方便處理的.該Demo採用的標記位是int類型的標記tag,每組的標記以此+1,每五個城市分爲一組,每組的第一個城市當作頭部局顯示的內容.咱們的分組頭部的高度爲40dp.
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (citiList == null || citiList.size() == 0) {
return;
}
int adapterPosition = parent.getChildAdapterPosition(view);
RecBean.CitiListBean beanByPosition = getBeanByPosition(adapterPosition);
if(beanByPosition == null){
return;
}
int preTage = -1;
int tage = beanByPosition.getTage();
//必定要記住這個 >= 0
if(adapterPosition - 1 >= 0) {
RecBean.CitiListBean nextBean = getBeanByPosition(adapterPosition - 1);
if (nextBean == null) {
return;
}
preTage = nextBean.getTage();
}
if(preTage != tage){
outRect.top = headHeight;
}else {
//這個目的是留出分割線
outRect.top = lineHeight;
}
}
複製代碼
這樣下來咱們給分組頭部的空間就預留出來了.接下來繪製分組頭部,由於分割線我直接顯示的背景色因此就不用去繪製分割線了.
上代碼:
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if(citiList == null || citiList.size() == 0){
return;
}
int parentLeft = parent.getPaddingLeft();
int parentRight = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
int tag = -1;
int preTag;
for (int i = 0; i <childCount; i++) {
View childView = parent.getChildAt(i);
if(childView == null){
continue;
}
int adapterPosition = parent.getChildAdapterPosition(childView);
當前Item的Top
int top = childView.getTop();
int bottom = childView.getBottom();
preTag = tag;
tag = citiList.get(adapterPosition).getTage();
//判斷下一個是否是分組的頭部
if(preTag == tag){
continue;
}
//這裏面我把每一個分組的頭部顯示的文字列表單獨提出來了,爲了測試方便用,
String name = index.get((tag - 1 ) < 0 ? 0 : (tag -1));
int height = Math.max(top,headHeight);
//判斷下一個Item是不是分組的頭部
if(adapterPosition + 1 < citiList.size()){
int nextTag = citiList.get(adapterPosition + 1).getTage();
if(tag != nextTag){
//這裏就是實現漸變效果的地方
//由於若是遍歷到
height = bottom;
}
}
paint.setColor(Color.parseColor("#ffffff"));
c.drawRect(parentLeft,height - headHeight,parentRight,height,paint);
paint.setColor(Color.BLACK);
paint.getTextBounds(name, 0, name.length(), rectOver);
c.drawText(name, dip2px(10), height - (headHeight - rectOver.height()) / 2, paint);
}
}
複製代碼
到這裏咱們的功能已經結束了,咱們要知道getItemOffsets()會提早執行,每一個Item的回收和出現都會執行一次.onDraw或者onDrawOver再屏幕中的Item發生變化的時候都會執行,只要發生變化.咱們的Head會不停的繪製.
這是2018年的第一篇文章,以前太忙了也沒好好的總結知識點.寫的倉促但願你們多多指導文章出現的問題,謝謝你們的反饋,歡迎評論吐槽哦~