RecyclerView的痛点使用

前言

申明:这里不提供封装好的 RecylerView 库,碍于作者能力,只能提供一些思路和想法。同时,这不是一篇基础性的 RecylerView 的使用文章,文章中我将主要对我本人在使用过程遇到的通点进行归纳,并提供自己的解决方法。
众所周知,在 Android 5.0 之后,Google 引入 Material Design 设计,同时为我们带来了 ListView 的替代品 RecylerView。对比 ListView, RecyclerView 在可扩展性,灵活性上更方便我们自己去定制。话不多说,下面我将从下拉刷新上拉加载更多自定义分割线item点击事件阐述我的跟人想法。

下拉刷新

本着用最简便的方式实现需求的原则,我们在下拉刷新这块主要是借助 Google 提供的 SwipeRefreshLayout 控件。
SwipeRefreshLayout 使用起来很简单,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp" />
</android.support.v4.widget.SwipeRefreshLayout>

实现下拉刷新,只需要调用SwipeRefreshLayout的监听事件 OnRefreshListener,在监听事件方法里实现响应的逻辑就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
texts.clear();
i = 0;
setData();
mAdapter.notifyDataSetChanged();
mRefresh.setRefreshing(false);
}
},2000);
}
});

自定义分割线

使用 RecyclerView 时,为了控制 item 之间的距离,我们既可以通过设置 item 的 margin 调整 item 之间的距离,也可以调用 RecyclerView 的 addItemDecoration() 方法设置分割线来控制 item 之间的距离。这里我们使用的是后者。
我们需要自定义一个分割线类继承自 RecyclerView.ItemDecoration,重写 onDraw() 方法(绘制分割线)以及 getItemOffsets() 方法(设置分割线的大小)。
此处给出我的自定义分割线源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private int orientation;
//转换成整型后分割线尺寸
private int itemSize;
//待传入的分割线尺寸,如果没有传入,初始化的时候默认16
private float size;
private Paint mPaint;
private Context context;
public DividerItemDecoration(Context context) {
this(context, 16, LinearLayoutManager.VERTICAL);
}
public DividerItemDecoration(Context context, float size) {
this(context, size, LinearLayoutManager.VERTICAL);
}
public DividerItemDecoration(Context context, int orientation) {
this(context, 16, orientation);
}
public DividerItemDecoration(Context context, float size, int orientation) {
if (orientation != LinearLayoutManager.VERTICAL && orientation != LinearLayoutManager.HORIZONTAL) {
throw new IllegalArgumentException("the orientation is not VERTICAL or HORIZONTAL!");
}
this.context = context;
this.size = size;
this.orientation = orientation;
initAttrs();
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (orientation == LinearLayoutManager.VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (orientation == LinearLayoutManager.VERTICAL) {
outRect.set(0, 0, 0, itemSize);
} else {
outRect.set(0, 0, itemSize, 0);
}
}
private void initAttrs() {
itemSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, context.getResources().getDisplayMetrics());
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.TRANSPARENT);
mPaint.setStyle(Paint.Style.FILL);
}
/**
* 绘制横向item分割线
*
* @param canvas 画布
* @param parent recyclerView
*/
private void drawVertical(Canvas canvas, RecyclerView parent) {
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
int top = child.getBottom() + params.bottomMargin;
int bottom = top + itemSize;
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
/**
* 绘制横向item分割线
*
* @param canvas 画布
* @param parent recyclerView
*/
public void drawHorizontal(Canvas canvas, RecyclerView parent) {
int top = parent.getPaddingTop();
int bottom = parent.getHeight() - parent.getPaddingBottom();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
int left = child.getRight() + params.rightMargin;
int right = left + itemSize;
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
}

item点击事件

RecyclerView 本身没有给我们提供类似 ListView 的 setOnItemClickListener 的监听事件,所以我们只能自己去实现这样一个监听事件。
在我们定义的 Adapter 中定义一个 OnItemClickListener 接口,接口中定义一个 OnItemClick() 方法,之后我们需要对外提供一个方法去实现这个接口。

1
2
3
4
5
6
7
8
public interface OnItemClickListener {
void onItemClick(View view, int position);
}
private OnItemClickListener onItemClickListener;
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}

然后在 onBindViewHolder 中添加如下代码:

1
2
3
4
5
6
7
8
if (onItemClickListener != null) {
((ItemViewHolder) holder).itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClickListener.onItemClick(((ItemViewHolder) holder).itemView, holder.getAdapterPosition());
}
});
}

最后,如果需要给 item 实现点击事件,利用 adapter.setOnItemClickListener() 实现就可以了

1
2
3
4
5
6
mAdapter.setOnItemClickListener(new RecyclerAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
Toast.makeText(MainActivity.this, "click " + (position + 1), Toast.LENGTH_SHORT).show();
}
});

上拉加载更多

上拉加载更多这块,主要是给 RecyclerView 添加一个底部布局。 RecyclerView.Adapter 给我们提供了 getItemViewType 方法来告诉我们每个 item 是属于正常布局还是属于底部布局

1
2
3
4
5
6
7
@Override
public int getItemViewType(int position) {
if (position == texts.size()) {
return FOOT_TYPE;//说明该item是底部
}
return NORMAL_TYPE;
}

同时为了实现加载数据,我们通过调用 RecyclerView 的 addOnScrollListener() 方法,实现 onScrolled() 方法和 onScrollStateChanged() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
int itemLastIndex = mAdapter.getItemCount() - 1;//recyclerView最后一项位置
if (newState == RecyclerView.SCROLL_STATE_IDLE && visibleLastIndex == itemLastIndex) {
if (i < 100) {
mAdapter.setLoading(true);
mAdapter.notifyDataSetChanged();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
setData();
mAdapter.setLoading(false);
mAdapter.notifyDataSetChanged();
}
}, 2000);
} else {
mAdapter.setAll(true);
mAdapter.notifyDataSetChanged();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
visibleLastIndex = manager.findLastCompletelyVisibleItemPosition();
}
});

简单说下思路,调用此监听事件后,当 RecyclerView 在滑动过程中会不断回调一些数据来表示当前 RecyclerView 的状态。我们为了实现上拉加载更多,我们需要知道,当 RecyclerView 滑到底部时,同时继续向上滑动我们就需要加载更多数据,所以在 onScrolled() 方法中我们利用 findLastCompleteVisibleItemPosition() 方法来获得当前 RecyclerView 停止滑动时最后一个可见项的位置是多少,当 RecyclerView 在滑动状态改变时,我们判断如果 RecylerView 改变的状态是停止滑动同时当前可见项位置是 Adapter 除底部项外最后一个,那么这个时候我们就需要加载数据。