学习资料:
- 张旭童同学的使用ItemDecoration为RecyclerView打造带悬停头部的分组列表
- Piasy大神的深入理解 RecyclerView 系列之一:ItemDecoration
Piasy大神的每篇博客质量都很高,强烈推荐
网上有很多关于RecyclerView
学习博客,之前看了几篇,但基本侧重点都是RecyclerView.Adapter
。关于RecyclerView
的侧滑删除,之前有过简单学习ItemTouchHleper实现RecyclerView侧滑删除,但对RecyclerView
了解远远不够。除了Adapter
外,RecyclerView
还有很多其他强大的地方需要学习
天才木木同学收集整理的的Android开发之一些好用的RecyclerView轮子非常好
学习计划:
- ItemDecoration
- LayoutManager
- RecyclerView.Adapter
- DiffUtil
- SimpleOnItemTouchListener
- SmoothScroller
- ItemAnimator
1. ItemDecoration 条目装饰
是一个抽象类,顾名思义,就是用来装饰RecyclerView
的子item
的,通过名字就可以知道,功能并不仅仅是添加间距绘制分割线,是用来装饰item
的。源码中的描述:
An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.
基本的功能是可以用来给RecyclerView
的子item
设置四边边距,以及上下左右绘制分割线。当然功能不止这些
ItemDecoration
一个有6个抽象方法,有3个还废弃了,也就剩下3个需要学习
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) 设置四边边距
- onDraw(Canvas c, RecyclerView parent, State state) 绘制装饰
- onDrawOver(Canvas c, RecyclerView parent, State state) 绘制蒙层
1.1 使用RecyclerView展示50条字符串数据
直接使用RecyclerView
展示50条纯字符串数据,代码:
public class MainActivity extends AppCompatActivity {
private RecyclerView rv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
rv = (RecyclerView) findViewById(R.id.rv_main_activity);
//设置布局管理器
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
rv.setLayoutManager(manager);
//设置ItemDecoration
//适配器
RecyclerViewAdapter adapter = new RecyclerViewAdapter(rv, R.layout.item_layout);
rv.setAdapter(adapter);
//添加数据
addData(adapter);
}
/**
* 添加数据
*/
private void addData(RecyclerViewAdapter adapter) {
List listData = new ArrayList<>();
for (int i = 0; i < 50; i++) {
listData.add("英勇青铜5---->"+i);
}
adapter.setData(listData);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != rv) {
rv.setAdapter(null);
}
}
}
代码中没有为RecyclerView
设置ItemDecoration
,LayoutManager
为LineatLayoutManager
子item布局文件:
布局也特别简单,给TextView
设置了背景色,字体是白色
运行效果:
item
间就没有间距,也没有任何的分割线,TextView
背景色导致整个RecyclerView
看起来都设置了背景色
下面为每个item
底部添加间距
1.2 getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) 设置四边偏移量
自定义一个RVItemDecoration
继承ItemDecroation
,重写getItemOffsets()
代码:
public class RVItemDecoration extends RecyclerView.ItemDecoration {
private static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;//水平方向
private static final int VERTICAL = LinearLayoutManager.VERTICAL;//垂直方向
private int orientation;//方向
private final int decoration;//边距大小 px
public RVItemDecoration(@LinearLayoutCompat.OrientationMode int orientation int orientation, int decoration) {
this.orientation = orientation;
this.decoration = decoration;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
final int lastPosition = state.getItemCount() - 1;//整个RecyclerView最后一个item的position
final int current = parent.getChildLayoutPosition(view);//获取当前要进行布局的item的position
Log.e("0000", "0000---->" + current);
Log.e("0000", "0000state.getItemCount()---->" + state.getItemCount());
Log.e("0000", "0000getTargetScrollPosition---->" + state.getTargetScrollPosition());
Log.e("0000", "0000state---->" + state.toString());
if (current == -1) return;//holder出现异常时,可能为-1
if (layoutManager instanceof LinearLayoutManager && !(layoutManager instanceof GridLayoutManager)) {//LinearLayoutManager
if (orientation == LinearLayoutManager.VERTICAL) {//垂直
outRect.set(0, 0, 0, decoration);
if (current == lastPosition) {//判断是否为最后一个item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, 0, decoration);
}
} else {//水平
if (current == lastPosition) {//判断是否为最后一个item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0,decoration, 0);
}
}
}
}
}
在Acivity
中,初始化RecyclerView
的时候使用:
//设置ItemDecoration
rv.addItemDecoration(new RVItemDecoration(LinearLayoutManager.VERTICAL,30));
运行后效果
由于是入门学习,暂时也只是针对对LinearLayoutManager
做了一点简单处理,最后1个item
不再添加底部间距。实际开发的时候考虑的就要比这复杂的多。LinearLayoutManager
大部分时候考虑item
的position
就可以,但GridLayoutManager
和StaggeredGridLayoutManager
需要考虑行和列
,情况就比较复杂。
方法中有4个参数
- Rect outRect:可以简单理解为
item
四边边距奉封装在这个对象中,用来设置Item
的padding
- View view: childView,就是item,可以理解为item的根
View
,并不是item中的控件 - RecyclerView parent:就是
RecyclerView
自身 - RecyclerView.State state : RecyclerView的状态,但并不包含滑动状态
1.2.1 RecyclerView.State
这个类是RecyclerView
的一个静态内部类,源码中的解释:
Contains useful information about the current RecyclerView state like target scroll position or view focus. State object can also keep arbitrary data, identified by resource ids.
个人理解:
这个State
封装着RecyclerView
当前的状态,例如滑动目标的Position
或者子控件的焦点。State
对象也可以对任意的数据通过资源id
进行保存或者识别
在State
中有3个用于标记当前所处步骤的常量值:
-
STEP_START
:布局开始 -
STEP_LAYOUT
:布局中 -
STEP_ANIMATIONS
:处于动画中
RecyclerView
的工作流程肯定也会是measure,layout,draw
。3个值在RecyclerView
的onMeasure()
有使用,感觉是用来标识RecyclerView
在测量过程中所处于的不同时机。目前并不清楚具体的影响,RecyclerView
工作流程需要以后再进行深入学习
方法 | 作用 |
---|---|
getItemCount() |
得到整个RecyclerView 中,目前的item 的数量 |
isMeasuring() |
是否正在测量 |
isPreLayout() |
是否准备进行布局 |
get(int resourceId) |
根据资源id 获取item 中的控件,建议使用R.id.* |
put(int resourceId, Object data) |
添加一个指定id 映射的资源对象,建议使用R.id.* 来避免冲突 |
remove(int resourceId) |
根据使用R.id.* 指定id 来删除存入的控件对象 |
getTargetScrollPosition() |
返回已经可见的滑动目标在Adapter 的索引值,滑动目标由SmoothScroller 来指定 |
hasTargetScrollPosition() |
判断是否已经滑动到目标 |
willRunPredictiveAnimations() |
判断是否进行预测模式的动画在布局过程中 |
willRunSimpleAnimations() |
判断是否进行简单模式的动画在布局过程中 |
getItemCount()
并不是完全等于getAdapter.getItemCount()
,在源码的注释中,关于postion
的计算,建议使用State.getItemCount()
而非立即直接通过Adapter
State
有些方法和属性涉及到其他的类,有些涉及RecyclerView
的工作过程,目前我的学习程度也不是很了解,暂时并不打算继续深挖学习下去,总觉得理解有错误,知道的同学请指出
1.3 onDraw(Canvas c, RecyclerView parent, State state)绘制装饰
这个用于绘制divider
,绘制在item
的下一层,也就是说item
会盖在divider
所在层的上面
使用重写了onDrawer()
方法和onDrawOver()
的ItemDecoration
后,对RecyclerView
在绘制item
时有些影响,主要是由于绘制顺序:
mItemDecoration.onDraw()-->item.onDraw()--->mItemDecoration.onDrawOver()
onDraw()
方法可以为divier
设置绘制范围,并且绘制范围可以超出在 getItemOffsets
中设置的范围,但由于是在item
下面一层进行绘制,会存在overdraw
简单使用,完整代码
public class RVItemDecoration extends RecyclerView.ItemDecoration {
private final int orientation;//方向
private final int decoration;//边距大小 px
private final int lineSize ;//分割线厚度
private final ColorDrawable mDivider;
public RVItemDecoration(@LinearLayoutCompat.OrientationMode int orientation, int decoration, @ColorInt int color, int lineSize) {
mDivider = new ColorDrawable(color);
this.orientation = orientation;
this.decoration = decoration;
this.lineSize = lineSize;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
final int lastPosition = state.getItemCount() -1;//整个RecyclerView最后一个item的position
final int current = parent.getChildLayoutPosition(view);//获取当前要进行布局的item的position
if (current == -1) return;
if (layoutManager instanceof LinearLayoutManager && !(layoutManager instanceof GridLayoutManager)) {//LinearLayoutManager
if (orientation == LinearLayoutManager.VERTICAL) {//垂直
if (current == lastPosition) {//判断是否为最后一个item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, 0, decoration);
}
} else {//水平
if (current == lastPosition) {//判断是否为最后一个item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, decoration, 0);
}
}
}
}
/**
* 绘制装饰
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
if (orientation == LinearLayoutManager.VERTICAL) {//垂直
drawHorizontalLines(c, parent);
} else {//水平
drawVerticalLines(c, parent);
}
}
/**
* 绘制垂直布局 水平分割线
*/
private void drawHorizontalLines(Canvas c, RecyclerView parent) {
// final int itemCount = parent.getChildCount()-1;//出现问题的地方 下面有解释
final int itemCount = parent.getChildCount();
Log.e("item","---->"+itemCount);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < itemCount; i++) {
final View child = parent.getChildAt(i);
if (child == null) return;
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top +lineSize;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 绘制水平布局 竖直的分割线
*/
private void drawVerticalLines(Canvas c, RecyclerView parent) {
final int itemCount = parent.getChildCount();
final int top = parent.getPaddingTop();
for (int i = 0; i < itemCount; i++) {
final View child = parent.getChildAt(i);
if (child == null) return;
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int bottom = child.getHeight() - parent.getPaddingBottom();
final int left = child.getRight() + params.rightMargin;
final int right = left +lineSize;
if (mDivider == null) return;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
运行后的效果:
同样这里也只是考虑了最简单的
LinerLayoutManager
一种情况。使用这个方法时,注意绘制范围,尽量避免
overdraw
当间距小于分割线的宽度时,分割线绘制的厚度会保持与间距一样
1.3 onDrawOver(Canvas c, RecyclerView parent, State state) 绘制蒙层
这个方法是在item
的onDraw()
方法之后进行回调,也就绘制在了最上层
简单使用,绘制一个颜色红黄渐变的圆
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
//画笔
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//圆心 x 坐标
final float x = parent.getWidth() / 2;
////圆心 y 坐标
final float y = 100;
//半径
final float radius = 100;
//渐变着色器 坐标随意设置的
final LinearGradient shader = new LinearGradient(x-50, 0, x+100, 200, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT);
paint.setShader(shader);
//绘制圆
c.drawCircle(x, y, radius, paint);
}
只要手指在RecyclerView
上进行滑动,onDrawOver()
方法就会被回调。但onDrawOver()
每回调一次,会将上次的绘制清除,只有最后一次的绘制会被保留。也就是说绘制的蒙层在屏幕只会有一个
2. 遇到的问题
在绘制底部分割线的时候,遇到一个问题:
当快速滑动时,底部会闪动,造成体验不好,如果分割线比较窄,不是很明显,分割线宽的时候就很明显
已解决 ,原因分析在下面
2.1 补充,问题修复
问题原因:
问题出在drawHorizontalLines()
方法中final int itemCount = parent.getChildCount()-1
这行代码,之所以减一考虑的是为了使最后一个item
下,不用再绘制分割线。
RecyclerView.getChildCount()
方法的返回值并不是recyclerView
的Adapter
中所有的item
的数量,而是当前屏幕中出现在RecyclerView
中item
的数量,一个item
只要露出一点点,就算出现,就会被包含在内。
-1
就会导致RecycelrView
统计已经出现的item
时的数量少一个,就会导致滑动过程中,屏幕中最后一个item
的底部分割线不进行绘制,造成闪屏
解决办法:
不减1,就OK,修改为:
final int itemCount = parent.getChildCount();
注意:
ViewGroup
的getChildCount()
方法的返回值itemCount
便是 getChildAt(int index)
这个方法index
的区间上限 ,[0,itemCount)
。例如:
当前屏幕显示的是
25--到-->42
,
parent.getChildCount()
的返回结果
itemCount
便是
18
。凡是在屏幕上第一个出现的
item
的
index
便是
0
,哪怕只是漏出一点点。在
parent.getChildAt(int index)
中,
index
的取值范围便是
0<= index < 18
2016.10.17 13:48
3.0 补充 官方推出DividerItemDecoration
2016.10.20
Android support libraries
更新了25.0.0
,新增了BottomNavigationView
,并增加了一个官方版的DividerItemDecoration
,可以学习下代码,有一些不错的细节优化
以上信息从drakeet 博客得知,果然关注大神,能够多了解信息
3. 最后
作为一个青铜5
的选手,也是热爱LOL
的,也有着一颗王者
心,可RNG,EDG
全输了,止步8强,郁闷
本人很菜,有错误请指出
一个完整的练习:TitleItemDecoration
慕课有一个不错的视屏不一样的RecyclerView优雅实现复杂列表布局
共勉 :)