前兩篇介紹完了ItemDecoration的基本用法。這次打算用ItemDecoration
做點好玩的——實現(xiàn)stickyHeader
效果。如圖:
我們從動畫可以看出,其實頭部有種,一種跟ItemView
在同一層級,類似一個不同type
的Item
;另外一個始終保持在最上層,并且滑動到每組的最后一個item
時,會有一個被頂上去,或者被拉下來的效果。
我們繼續(xù)分析下,應(yīng)該如何實現(xiàn):
與
itemView
同一層級的header
,我們可以把它當做是一個divider
,通過getItemOffsets
和onDraw
來處理,且每組第一個Item
的上方才顯示。浮在
itemView
上層的header
,很明顯可以通過onDrawOver
來實現(xiàn)。-
浮在
itemView
上層的header
被頂上去和被拉下來的效果,只需要當每組最后一個Item
的bottom
小于header
的height
時,讓header
跟隨這個item
就行,換句話說就是此時讓header
的bottom
等于firstItem
的bottom
。(有點繞的話,結(jié)合下面的圖看看)
還有最后一個問題需要確認,那就是哪個item
是每組的開始,哪個Item
是每組的結(jié)束。 這個問題跟我們能拿到的數(shù)據(jù)集合有關(guān)。有可能拿到的是類似IOS那樣:title
是List<A>
集合,Item是分好組的List<List<B>>
集合(IOS的小伙伴說的,應(yīng)該沒騙我,indexPath
我聽過);也有可能拿到就是一個List<B>
集合,泛型 B
有個字段作為title
,title
相同便是同一分組??傊还軘?shù)據(jù)集合是怎么樣的,我們都需要確認那個item
是每組的開始,哪個item是每組的結(jié)束。假定我們拿到的就是一個List<B>
形式的集合,item的實體類如下:
public class AppInfoBean {
public String name;
public String url;
public String tag;
public AppInfoBean(String name, String url,String tag) {
this.name = name;
this.url = url;
this.tag = tag;
}
}
具體應(yīng)該如何判斷呢,首先,position
為0肯定是第一組的開始;position
為List.size()-1
肯定是最后一組的結(jié)束;其他position
,如果當前的tag
與上一個position
的tag
不等,那么肯定是某組的開始,如果當前的tag
與下一個position
的tag
不等,那么肯定是某組的結(jié)束。定義一個類來描述每個Item的開始,結(jié)束狀態(tài):
public class SectionBean {
public boolean isGroupStart;
public boolean isGroupEnd;
}
上面的分析,可以總結(jié)成如下代碼:
SectionBean tag;
if (position == 0) {
tag.isGroupStart = true;
tag.isGroupEnd = !mDatas.get(position).tag.equals(mDatas.get(position + 1).tag);
} else if (position == mDatas.size() - 1) {
tag.isGroupStart = !mDatas.get(position).tag.equals(mDatas.get(position - 1).tag);
tag.isGroupEnd = true;
} else {
tag.isGroupStart = !mDatas.get(position).tag.equals(mDatas.get(position - 1).tag);
tag.isGroupEnd = !mDatas.get(position).tag.equals(mDatas.get(position + 1).tag);
}
至于這個判斷放在什么地方,就看大家的見解了。我這里選擇在adapter
的onBindViewHolder
中生成每個position
對應(yīng)的SectionBean
,通過itemview
的setTag
方法存起來,這樣的話,在ItemDecoration
可以通過getTag
取出來,具體代碼如下:(當然直接在ItemDecoration
中做判斷也可以)
@Override
public void onBindViewHolder(StickHeaderVH holder, int position) {
holder.sdvPic.setImageURI(mDatas.get(position).url);
holder.tvTitle.setText(mDatas.get(position).name);
generateTag(holder, position);
}
private void generateTag(StickHeaderVH holder, int position) {
SectionBean tag;
// 沒有tag的話 new 一個, 有的話 復(fù)用
if (holder.itemView.getTag() == null) {
tag = new SectionBean();
} else {
tag = (SectionBean) holder.itemView.getTag();
}
// 判斷當前position的開始結(jié)束狀態(tài)
if (position == 0) {
tag.isGroupStart = true;
tag.isGroupEnd = !mDatas.get(position).tag.equals(mDatas.get(position + 1).tag);
} else if (position == mDatas.size() - 1) {
tag.isGroupStart = !mDatas.get(position).tag.equals(mDatas.get(position - 1).tag);
tag.isGroupEnd = true;
} else {
tag.isGroupStart = !mDatas.get(position).tag.equals(mDatas.get(position - 1).tag);
tag.isGroupEnd = !mDatas.get(position).tag.equals(mDatas.get(position + 1).tag);
}
holder.itemView.setTag(tag);
}
經(jīng)過最開始的分析,與ItemView
同一層級的header
其實就是一個有文字的divider
,只有每組的第一個Item
才顯示,這個很好處理,代碼如下(文字居中的處理其實有很多細節(jié)可以摳的,有機會單獨列出):
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
// 如果是頭部
if (((SectionBean) view.getTag()).isGroupStart) {
outRect.set(0, (int) mDividerHeight, 0, 0);
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
// 如果是頭
if (position != RecyclerView.NO_POSITION
&& ((SectionBean) view.getTag()).isGroupStart) {
drawHeader(c, parent, view, position);
}
}
}
/**
* 畫頭部
*
* @param c
* @param parent
* @param view
* @param position
*/
private void drawHeader(Canvas c, RecyclerView parent, View view, int position) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
int bottoom = view.getTop() - params.topMargin - Math.round(ViewCompat.getTranslationY(view));
int top = (int) (bottoom - mDividerHeight);
// 計算文字居中時候的基線
Rect targetRect = new Rect(left, top, right, bottoom);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
c.drawRect(left, top, right, bottoom, mPaint);
c.drawText(mDatas.get(position).tag, left, baseline, mTextPaint);
}
接下來再處理浮在頂層的header
,上面已經(jīng)分析過了,這個header
一般情況下都是固定在recyclerView
的頂部,只有達到臨界點后,其底部才會跟隨第一個可見ItemView
的底部,所以我們只需要著重留意下臨界點就行了,代碼如下:
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
View view = parent.getChildAt(0);
View view2 = parent.getChildAt(1);
if (view != null && view2 != null) {
SectionBean section1 = (SectionBean) view.getTag();
SectionBean section2 = (SectionBean) view2.getTag();
int position = parent.getChildAdapterPosition(view);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
int bottoom = (int) mDividerHeight;
int top = 0;
// 判斷是否達到臨界點
// (第一個可見item是每組的最后一個,第二個可見tiem是下一組的第一個,并且第一個可見item的底部小于header的高度)
// 這里直接判斷item的底部位置小于header的高度有點欠妥,應(yīng)該還要考慮paddingtop以及margintop,這里展示不考慮了
if (section1.isGroupEnd && section2.isGroupStart && view.getBottom() <= mDividerHeight) {
bottoom = view.getBottom();
top = (int) (bottoom - mDividerHeight);
}
// 計算文字居中時候的基線
Rect targetRect = new Rect(left, top, right, bottoom);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
// 背景
c.drawRect(left, top, right, bottoom, mPaint);
// 文字
c.drawText(mDatas.get(position).tag, left, baseline, mTextPaint);
}
}
這樣基本上就OK了, 唯一的遺憾就是header不能點擊啊。。。。源碼點我