相信大家对于RecyclerView
现在使用应该相当普遍了吧,它作为ListView
的有力替代品,有着很好的扩展性。
我们在ListView
中增加分割线的时候,会只用android:divider
,而在RecyclerView
中是没有这个属性的。代替品是ItemDecoration
,当然ItemDecoration
并不是只可以作为分割线使用的。网上已经有很多优秀的使用了,这里我列出来几个github上的。
制作角标: blog.csdn.net/yu_duan_hun…
时间线效果: github.com/vivian87251…
各种样式的分割线: github.com/yqritc/Recy…
1、ItemDecoration的基本使用
mMainAdapter = new MainAdapter(this, entities);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.addItemDecoration(new StickyItemDecoration.Builder(this).create(entities));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
recyclerView.setAdapter(mMainAdapter);
可以看到在设置addItemDecoration
时候,是可以设置多个ItemDecoration.
的,绘制顺序是从按照插入顺序绘制。
2、如何自定义ItemDecoration
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) ;
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) ;
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) ;
在自定义ItemDecoration
的时候,主要可以使用到下面三个方法。
-
getItemOffsets
该方法中又一个outRect
表示当前View
的边界值,当我们设置他的上下左右边界时,会加入一个类似margin的操作,将布局撑开。下面是一个从别人博客那过来的图。默认情况下outRect的上下左右值都是0,当outRect值变化时,会相对应的撑开布局。
-
onDraw 这个方法实际上就是真实的绘制操作了,可以用canvas进行绘制,在
getItemOffsets
中,对布局撑开处理了之后,就可以在撑开的部分进行绘制。该操作的绘制是和itemview在同一层。 -
onDrawOver 这个方法也是用来绘制的,但是它的绘制是在itemview的上层,体现效果就是会覆盖在itemview的上层。
4、系统自带的DividerItemDecoration源码分析
public class DividerItemDecoration extends ItemDecoration {
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{16843284};
private Drawable mDivider;
private int mOrientation;
private final Rect mBounds = new Rect();
public DividerItemDecoration(Context context, int orientation) {
//获取的是主题中的@android:attr/listDivider属性
TypedArray a = context.obtainStyledAttributes(ATTRS);
this.mDivider = a.getDrawable(0);
if (this.mDivider == null) {
Log.w("DividerItem", "@android:attr/listDivider was not set in the theme used for this DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
//设置传入的绘制方向
this.setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != 0 && orientation != 1) {
throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
} else {
this.mOrientation = orientation;
}
}
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
} else {
this.mDivider = drawable;
}
}
public void onDraw(Canvas c, RecyclerView parent, State state) {
if (parent.getLayoutManager() != null && this.mDivider != null) {
//根据传入的方向进行区分绘制
if (this.mOrientation == 1) {
this.drawVertical(c, parent);
} else {
this.drawHorizontal(c, parent);
}
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
int left;
int right;
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
int childCount = parent.getChildCount();
for(int i = 0; i < childCount; ++i) {
View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, this.mBounds);
int bottom = this.mBounds.bottom + Math.round(child.getTranslationY());
int top = bottom - this.mDivider.getIntrinsicHeight();
//绘制操作,由于是使用的Drawable所以可以使用shape,也可以使用图片
this.mDivider.setBounds(left, top, right, bottom);
this.mDivider.draw(canvas);
}
canvas.restore();
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
int top;
int bottom;
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
int childCount = parent.getChildCount();
for(int i = 0; i < childCount; ++i) {
View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, this.mBounds);
int right = this.mBounds.right + Math.round(child.getTranslationX());
int left = right - this.mDivider.getIntrinsicWidth();
this.mDivider.setBounds(left, top, right, bottom);
this.mDivider.draw(canvas);
}
canvas.restore();
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
if (this.mDivider == null) {
outRect.set(0, 0, 0, 0);
} else {
if (this.mOrientation == 1) {
outRect.set(0, 0, 0, this.mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, this.mDivider.getIntrinsicWidth(), 0);
}
}
}
}
可以参考洋神的文章。blog.csdn.net/lmj62356579…
3、实现一个带有顶部悬停效果的ItemDecoration
3-1、为RecyclerView插入字母分割条
插入分割线,从上面的分析需要做两部操作,撑开布局(getItemOffset)和在撑开的布局中绘制(onDraw)
- getItemOffset
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
//获取当前view在整个列表中的位置
int position = parent.getChildAdapterPosition(view);
//如果没有填充数据,直接跳过
if (groupEntities == null || groupEntities.size() == 0) return;
//第一个元素,直接需要绘制,给一个高度撑开
if (position == 0) {
outRect.set(0, mStickyHeight, 0, 0);
} else {
//如果两个view的实体类key值不同,则表示两个实体是不同组的,则添加分组的标签上去
if (null != groupEntities.get(position).getKey()
&& !groupEntities.get(position).getKey().equals(groupEntities.get(position - 1).getKey())) {
outRect.set(0, mStickyHeight, 0, 0);
}
}
}
- onDraw
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(canvas, parent, state);
//如果没有数据,直接跳过绘制
if (groupEntities == null || groupEntities.size() == 0) return;
//获取当前屏幕展示总的item数量
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
//根据当前的itemview,获取这个item在整个布局列表中的位置
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
int position = layoutParams.getViewLayoutPosition();
//计算出来绘制的悬停标签的文字大小
mTextPaint.getTextBounds(groupEntities.get(position).getKey(), 0, groupEntities.get(position).getKey().length(), textRect);
if (position == 0) {
//如果是第一个元素,无论如何都绘制一个标签上去
canvas.drawRect(new Rect(0, 0, child.getRight(), mStickyHeight), mPaint);
canvas.drawText(groupEntities.get(position).getKey(), mStickyLeftPadding, (mStickyHeight + textRect.height()) / 2, mTextPaint);
} else {
//如果元素是不同的分组,就在他们之间绘制一个标签
if (groupEntities.get(position).getKey() != null
&& !groupEntities.get(position).getKey().equals(groupEntities.get(position - 1).getKey())) {
canvas.drawRect(new Rect(0, child.getTop() - mStickyHeight, child.getRight(), child.getTop()), mPaint);
canvas.drawText(groupEntities.get(position).getKey(),
mStickyLeftPadding, child.getTop() - mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
}
}
}
}
到这里,就实现了如下操作,当前是不能悬停的。
3-2、实现悬停效果
在看代码之前,先用图来分析下,悬停效果的过程。
可以看到在滚动的时候,组内的最后一个元素的
getTop
是一个不断缩小的过程,当getTop
变为0的时候,最后一个布局元素就刚好是屏幕展示的第一个元素了,再缩小的话,getTop
就变为小于0的值,而当布局的高度-getTop=悬停布局高度的时候,刚好这里的B和C布局应该就是紧挨着的了。在缩小的话,就会C占据了B的位置。为了产生一个挤压的效果,在展示上面,B被C挤上去,所以使用canvas的平移,产生这个效果。好了分析完了,代码如下,很简单。
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);
//没有数据填充,跳过绘制
if (groupEntities == null || groupEntities.size() == 0) return;
//获取屏幕上第一个元素在整个布局中的位置
int firstVisiblePosition = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
//根据位置获取到这个元素对象
View firstVisibleChild = parent.findViewHolderForAdapterPosition(firstVisiblePosition).itemView;
//如果当前布局的两个实体不是同一个组,并且第一个布局展示出来的高度小于悬停view的高度,将canvas平移,实现出来挤压移动效果
if (groupEntities.get(firstVisiblePosition).getKey() != null
&& !groupEntities.get(firstVisiblePosition).getKey().equals(groupEntities.get(firstVisiblePosition + 1).getKey())
&& firstVisibleChild.getTop() + firstVisibleChild.getHeight() < mStickyHeight) {
//getTop在移动的时候,是一个从正---->负的过程
canvas.translate(0, firstVisibleChild.getTop() + firstVisibleChild.getHeight() - mStickyHeight);
}
//获取文字大小
mTextPaint.getTextBounds(groupEntities.get(firstVisiblePosition).getKey(), 0, groupEntities.get(firstVisiblePosition).getKey().length(), textRect);
//绘制标签布局
canvas.drawRect(new Rect(0, 0, firstVisibleChild.getRight(), mStickyHeight), mPaint);
canvas.drawText(groupEntities.get(firstVisiblePosition).getKey(), mStickyLeftPadding, mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
}
3-3、完整代码
public class StickyItemDecoration extends RecyclerView.ItemDecoration {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
private int mStickyLeftPadding;
private int mStickyHeight;
private List<GroupEntity> groupEntities;
private Rect textRect = new Rect();
public static class Builder {
private int stickyLeftPadding = -1;
private int stickyHeight;
private int stickyFontSize;
private int stickyFontColor;
private int stickyBackgroundColor;
private Context mContext;
public Builder(Context context) {
this.mContext = context;
}
public Builder setStickyLeftPadding(int stickyLeftPadding) {
this.stickyLeftPadding = stickyLeftPadding;
return this;
}
public Builder setSstickyHeight(int stickyHeight) {
this.stickyHeight = stickyHeight;
return this;
}
public Builder setSstickyFontSize(int stickyFontSize) {
this.stickyFontSize = stickyFontSize;
return this;
}
public Builder setSstickyFontColor(int stickyFontColor) {
this.stickyFontColor = stickyFontColor;
return this;
}
public Builder setSstickyBackgroundColor(int stickyBackgroundColor) {
this.stickyBackgroundColor = stickyBackgroundColor;
return this;
}
public StickyItemDecoration create(@NonNull List<GroupEntity> groupEntities) {
if (stickyLeftPadding == -1) {
stickyLeftPadding = dip2px(12);
}
if (stickyHeight == 0) {
stickyHeight = dip2px(44);
}
if (stickyFontSize == 0) {
stickyFontSize = sp2px(18);
}
if (stickyFontColor == 0) {
stickyFontColor = Color.parseColor("#333333");
}
if (stickyBackgroundColor == 0) {
stickyBackgroundColor = Color.parseColor("#e6dedd");
}
return new StickyItemDecoration(stickyLeftPadding, stickyHeight,
stickyFontSize, stickyFontColor, stickyBackgroundColor, groupEntities);
}
private int dip2px(int dipValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dipValue, mContext.getResources().getDisplayMetrics());
}
private int sp2px(int spValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spValue, mContext.getResources().getDisplayMetrics());
}
}
protected StickyItemDecoration(int stickyLeftPadding, int stickyHeight,
int stickyFontSize, int stickyFontColor,
int stickyBackgroundColor, List<GroupEntity> groupEntityList) {
mPaint.setColor(stickyBackgroundColor);
mPaint.setStyle(Paint.Style.FILL);
this.mStickyHeight = stickyHeight;
this.mStickyLeftPadding = stickyLeftPadding;
this.groupEntities = groupEntityList;
mTextPaint.setColor(stickyFontColor);
mTextPaint.setStyle(Paint.Style.STROKE);
mTextPaint.setTextSize(stickyFontSize);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
if (groupEntities == null || groupEntities.size() == 0) return;
if (position == 0) {
outRect.set(0, mStickyHeight, 0, 0);
} else {
if (null != groupEntities.get(position).getKey()
&& !groupEntities.get(position).getKey().
equals(groupEntities.get(position - 1).getKey())) {
outRect.set(0, mStickyHeight, 0, 0);
}
}
}
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
super.onDraw(canvas, parent, state);
if (groupEntities == null || groupEntities.size() == 0) return;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
int position = layoutParams.getViewLayoutPosition();
mTextPaint.getTextBounds(groupEntities.get(position).getKey(), 0,
groupEntities.get(position).getKey().length(), textRect);
if (position == 0) {
canvas.drawRect(new Rect(0, 0, child.getRight(), mStickyHeight), mPaint);
canvas.drawText(groupEntities.get(position).getKey(),
mStickyLeftPadding, (mStickyHeight + textRect.height()) / 2, mTextPaint);
} else {
if (groupEntities.get(position).getKey() != null
&& !groupEntities.get(position).getKey()
.equals(groupEntities.get(position - 1).getKey())) {
canvas.drawRect(new Rect(0, child.getTop() - mStickyHeight,
child.getRight(), child.getTop()), mPaint);
canvas.drawText(groupEntities.get(position).getKey(),
mStickyLeftPadding, child.getTop() - mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
}
}
}
}
@Override
public void onDrawOver(@NonNull Canvas canvas,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);
if (groupEntities == null || groupEntities.size() == 0) return;
int firstVisiblePosition = ((LinearLayoutManager) parent.getLayoutManager())
.findFirstVisibleItemPosition();
View firstVisibleChild = parent.findViewHolderForAdapterPosition(firstVisiblePosition).itemView;
if (groupEntities.get(firstVisiblePosition).getKey() != null
&& !groupEntities.get(firstVisiblePosition).getKey()
.equals(groupEntities.get(firstVisiblePosition + 1).getKey())
&& firstVisibleChild.getTop() + firstVisibleChild.getHeight() < mStickyHeight) {
canvas.translate(0, firstVisibleChild.getTop()
+ firstVisibleChild.getHeight() - mStickyHeight);
}
mTextPaint.getTextBounds(groupEntities.get(firstVisiblePosition).getKey(),
0, groupEntities.get(firstVisiblePosition).getKey().length(), textRect);
canvas.drawRect(new Rect(0, 0, firstVisibleChild.getRight(), mStickyHeight), mPaint);
canvas.drawText(groupEntities.get(firstVisiblePosition).getKey(),
mStickyLeftPadding, mStickyHeight / 2 + textRect.height() / 2, mTextPaint);
}
public static class GroupEntity {
private String key;
private String value;
public GroupEntity(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GroupEntity that = (GroupEntity) o;
return key != null ? key.equals(that.key) : that.key == null;
}
@Override
public int hashCode() {
return key != null ? key.hashCode() : 0;
}
}
}
使用很简答,可以直接使用链式操作
new StickyItemDecoration.Builder(this).create(entities)
可以使用的方法有
//设置文字距离左边距离
public Builder setStickyLeftPadding(int stickyLeftPadding) {
this.stickyLeftPadding = stickyLeftPadding;
return this;
}
//设置悬停布局高度
public Builder setSstickyHeight(int stickyHeight) {
this.stickyHeight = stickyHeight;
return this;
}
//设置文字大小
public Builder setSstickyFontSize(int stickyFontSize) {
this.stickyFontSize = stickyFontSize;
return this;
}
//设置文字颜色
public Builder setSstickyFontColor(int stickyFontColor) {
this.stickyFontColor = stickyFontColor;
return this;
}
//设置悬停布局背景颜色
public Builder setSstickyBackgroundColor(int stickyBackgroundColor) {
this.stickyBackgroundColor = stickyBackgroundColor;
return this;
}