ItemDecoration是recyclerView拓展的一个很好工具,支持咱们在recyclerView上面作各类操做,并且耦合性低,容易添加。这篇咱们先用ItemDecoration来作悬浮/粘性头部,后面还能够用ItemDecoration作时间轴,手机通信录联系人右侧字母导航栏。
老规矩,先上图。
java
github地址:https://github.com/qdxxxx/StickyHeaderDecoration
天气热,本github已安装空调,star便可免费享用~~git
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies { compile 'com.github.qdxxxx:StickyHeaderDecoration:1.0.1' }
NormalDecoration decoration = new NormalDecoration() {
@Override
public String getHeaderName(int pos) {
return //返回每一个分组头部名称;
}
};
自定义头部/悬浮头部layout】【自定义头部加载图片请用 loadImage()
方法】github
decoration.setOnDecorationHeadDraw(new NormalDecoration.OnDecorationHeadDraw() {
@Override
public View getHeaderView(int pos) {
return //返回自定义头部view;
}
});
头部点击事件web
decoration.setOnHeaderClickListener(new NormalDecoration.OnHeaderClickListener() {
@Override
public void headerClick(int pos) {
}
});
name | format | 中文解释 |
---|---|---|
setHeaderHeight | integer | 分组头部高度 |
setTextPaddingLeft | integer | 普通分组头部【只含文字】文字左边距 |
setTextSize | integer | 普通分组头部【只含文字】文字大小 |
setTextColor | integer | 普通分组头部【只含文字】文字颜色 |
setHeaderContentColor | integer | 普通分组头部【只含文字】文字背景颜色 |
onDestory | 清空数据集合/监听等 | |
*loadImage | String,integer,ImageView | 用来加载并刷新图片到分组头部【自定义头部很重要的方法!】 |
又要开始漫天代码的解刨了,非专业战斗人员…请务必耐着性子看。
首先咱们来划分几个主要的功能模块canvas
ItemDecoration
如下一个段落引用【带心情去旅行】的简书,写的很具体。api
先看下RecyclerView.ItemDecoration的源码(部分):网络
public static abstract class ItemDecoration {
...
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
里面是咱们经常使用的三个方法:maven
RecyclerView 的背景、onDraw绘制的内容、Item、onDrawOver绘制的内容,各层级关系以下:ide
表示感谢【带心情去旅行】svg
根据上面的讲解,咱们用getItemOffsets()
方法设置分组的item头部,咱们只要判断当前item和上一个item是否属于同一个group便可。
/*咱们为每一个不一样头部名称的第一个item设置头部高度*/
int pos = parent.getChildAdapterPosition(itemView); //获取当前itemView的位置
String curHeaderName = getHeaderName(pos); //根据pos获取分组头部名
if (pos == 0 || !curHeaderName.equals(getHeaderName(pos - 1))) {//若是当前位置为0,或者与上一个item头部名不一样的,都腾出头部空间
outRect.top = headerHeight; //设置itemView PaddingTop的距离
}
用onDrawOver()
来绘制分组头部,至关于绘制在item的界面之上(由于item已经设置了偏移)
和上述方法同样,咱们先得到每一个分组的位置,而后绘制文字便可(自定义layout亦是如此)
int childCount = recyclerView.getChildCount();//获取屏幕上可见的item数量
for (int i = 0; i < childCount; i++) {
View childView = recyclerView.getChildAt(i);
int pos = recyclerView.getChildAdapterPosition(childView); //获取当前view在Adapter里的pos
String curHeaderName = getHeaderName(pos); //根据pos获取要悬浮的头部名
int viewTop = childView.getTop() + recyclerView.getPaddingTop();
if (pos == 0 || !curHeaderName.equals(getHeaderName(pos - 1))) {//若是当前位置为0,或者与上一个item头部名不一样的,都腾出头部空间
//绘制每一个组头【奥迪上头的a(阿尔法罗密欧上头就不用绘制a),本田上头的b】
canvas.drawRect(left, viewTop - headerHeight, right, viewTop, mHeaderContentPaint);//绘制头部背景
canvas.drawText(curHeaderName, left + textPaddingLeft, viewTop - headerHeight / 2 + txtYAxis, mHeaderTxtPaint);//绘制文字,文字的基线能够看个人自定义菜单,有说到
if (headerHeight < viewTop && viewTop <= 2 * headerHeight) { //此判断是恰好2个头部碰撞,悬浮头部就要偏移
translateTop = viewTop - 2 * headerHeight;//悬浮头部须要偏移的距离(y轴方向)
}
stickyHeaderPosArray.put(pos, viewTop);//将头部信息放进array,【头部点击处理有讲解】
}
}
经过上面的方法,咱们就能绘制出每一个分组的头部。最后咱们绘制一次悬浮的头部
canvas.save();
canvas.translate(0, translateTop);
canvas.drawRect(left, 0, right, headerHeight, mHeaderContentPaint);
canvas.drawText(firstHeaderName, left + textPaddingLeft, headerHeight / 2 + txtYAxis, mHeaderTxtPaint);
// canvas.drawLine(0, headerHeight / 2, right, headerHeight / 2, mHeaderTxtPaint);//画条线看看文字居中不
canvas.restore();
头部点击这个一开始的确有点棘手,由于这个分组的头部是咱们额外绘制上的,就必需要经过本身的计算和存储头部信息。
咱们在绘制头部的时候,经过SparseArray
将头部信息存储集合里,可是每onDrawOver
的时候都要clear一下,确保头部数据正确。
最后经过GestureDetector
来处理用户触摸事件,根据用户触摸的y轴位置来判断SparseArray
是否包含该位置。
@Override//单击事件
public boolean onSingleTapUp(MotionEvent e) {
for (int i = 0; i < stickyHeaderPosArray.size(); i++) {
int value = stickyHeaderPosArray.valueAt(i);
float y = e.getY();
if (value - headerHeight <= y && y <= value) {//若是点击到分组头
if (headerClickEvent != null) {
headerClickEvent.headerClick(stickyHeaderPosArray.keyAt(i));
}
return true;
}
}
return false;
}
绘制自定义layout的头部有2个要点
咱们能够经过view.setDrawingCacheEnabled(true)
方法,经过cache将view转化为bitmap,在用headerView.getDrawingCache()
获取bitmap对象。
View headerView = headerDrawEvent.getHeaderView(firstPos);
headerView.measure(//measure布局
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
headerView.setDrawingCacheEnabled(true);
headerView.layout(0, 0, right, headerHeight);//布局layout
canvas.drawBitmap(headerView.getDrawingCache(), left, 0, null);
可是若是view里面包含图片的话,图片不太多是咱们事先存储好的,而是经过网络请求得到的图片url,而后再加载。因此这也是一个难点之一。
mRecyclerView.postInvalidate()
,从而间接性的手动调用onDrawOver()
方法,从新绘制已经加载好的图片。public void loadImage(final String url, final int pos, ImageView imageView) {
if (imgDrawableMap.get(url) != null) {//若是图片已经加载过了,而且已经存储
imageView.setImageDrawable(imgDrawableMap.get(url));
} else {
Glide.with(mRecyclerView.getContext()).load(url).into(new SimpleTarget<Drawable>() {
@Override
public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
headViewMap.remove(pos);//删除,从新更新
imgDrawableMap.put(url, resource);
mRecyclerView.postInvalidate();
}
});
}
}
更多详细功能请移步NormalDecoration
,并配合onDrawOver()
解析。
因此自定义layout有图片请务必使用loadImage()
方法,以便及时讲加载完的图片绘制到界面上。
GridGridDecoration也有2个难点突破
getItemOffsets
,不只仅是分组头public abstract class GridDecoration extends NormalDecoration {
private int itemTotalCount;
public GridDecoration(int itemTotalCount, int span) {
this.itemTotalCount = itemTotalCount;
for (int pos = 0; pos < itemTotalCount; pos++) {
/*咱们为每一个不一样头部名称的第一个item设置头部高度*/
String curHeaderName = getRealHeaderName(pos); //根据j获取要悬浮的头部名
if (!headerPaddingSet.contains(pos) && (pos == 0 || !curHeaderName.equals(getRealHeaderName(pos - 1)))) {//若是是分组头部
groupHeadPos.add(pos);
for (int i = 0; i < span; i++) {
headerPaddingSet.add(pos + i);
if (!curHeaderName.equals(getRealHeaderName(pos + i + 1))) {//若是下一个分组名称不一致,pass
break;
}
}
}
if (!curHeaderName.equals(getRealHeaderName(pos + 1)) && groupHeadPos.size() > 0) {
int preHeadPos = (int) ((TreeSet) (groupHeadPos)).last();
int padSpan = span - (pos - preHeadPos) % span;
headerSpanArray.put(pos, padSpan);
}
}
}
private Set<Integer> headerPaddingSet = new TreeSet<>(); //用来记录每一个头部的paddintTop信息
private Set<Integer> groupHeadPos = new TreeSet<>(); //记录每一个分组第一个头部的pos【用于计算当前组最后一个item的span】
private SparseArray<Integer> headerSpanArray = new SparseArray<>(); //用来记录每一个分组最后一个item的span
private GridLayoutManager.SpanSizeLookup lookup;
@Override
public void getItemOffsets(Rect outRect, View itemView, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, itemView, parent, state);
if (lookup == null) {
lookup = new GridLayoutManager.SpanSizeLookup() {//至关于weight
@Override
public int getSpanSize(int position) {
int returnSpan = 1;
int index = headerSpanArray.indexOfKey(position);
if (index >= 0) {
returnSpan = headerSpanArray.valueAt(headerSpanArray.indexOfKey(position)); //设置itemView PaddingTop的距离
}
return returnSpan;
}
};
final GridLayoutManager gridLayoutManager = (GridLayoutManager) parent.getLayoutManager();
gridLayoutManager.setSpanSizeLookup(lookup);
}
/*咱们为每一个不一样头部名称的第一个item设置头部高度*/
int pos = parent.getChildAdapterPosition(itemView); //获取当前itemView的位置
if (headerPaddingSet.contains(pos)) {
outRect.top = headerHeight; //设置itemView PaddingTop的距离
}
}
}
至此咱们的功能都已经描述结束,作了这个小功能的确收货很多,比较多的耗时在GridDecoration的设计,由于不清楚可以动态的设置Span,一开始是经过设置itemOffsets的paddingRight去计算的,而后还要计算下一个分组的头部,各类问题,因此之后作功能时候先看看有没有api能够操做的,这样来的更方便和容易。最后附上github望小伙伴们多多点赞哈。有建议和意见还望在评论出提出~~
https://github.com/qdxxxx/StickyHeaderDecoration