前兩篇介紹完了ItemDecoration的基本用法。這次打算用ItemDecoration做點好玩的——實現(xiàn)stickyHeader效果。如圖:

我們從動畫可以看出,其實頭部有種,一種跟ItemView在同一層級,類似一個不同type的Item;另外一個始終保持在最上層,并且滑動到每組的最后一個item時,會有一個被頂上去,或者被拉下來的效果。
我們繼續(xù)分析下,應(yīng)該如何實現(xiàn):
與
itemView同一層級的header,我們可以把它當(dāng)做是一個divider,通過getItemOffsets和onDraw來處理,且每組第一個Item的上方才顯示。浮在
itemView上層的header,很明顯可以通過onDrawOver來實現(xiàn)。-
浮在
itemView上層的header被頂上去和被拉下來的效果,只需要當(dāng)每組最后一個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,如果當(dāng)前的tag與上一個position的tag不等,那么肯定是某組的開始,如果當(dāng)前的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取出來,具體代碼如下:(當(dāng)然直接在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();
}
// 判斷當(dāng)前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不能點擊啊。。。。源碼點我
