您的位置:首页 > 移动开发 > Android开发

Android——RecyclerView入门学习之ItemDecoration

2017-04-04 12:14 411 查看

学习资料:

使用ItemDecoration为RecyclerView打造带悬停头部的分组列表
深入理解 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<String> 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布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="20sp" />
</LinearLayout>

布局也特别简单,给
TextView
设置了背景色,字体是白色

运行效果:



不设置ItemDecroation

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)
。例如:



position示例

当前屏幕显示的是
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优雅实现复杂列表布局
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: