浅析RecyclerView

前言

RecyclerView是v7包中的一个控件,用于替代之前的ListView。性能上比ListView更好,使用上也更灵活。以下是官方的说明:

The RecyclerView widget is a more advanced and flexible version of ListView. This widget is a container for displaying large data sets that can be scrolled very efficiently by maintaining a limited number of views. Use the RecyclerView widget when you have data collections whose elements change at runtime based on user action or network events.

导入依赖

使用前需要导入依赖:

1
2
3
4
dependencies {
... ...
implementation 'com.android.support:recyclerview-v7:latest-vision'
}

简单使用

在layout中添加RecyclerView

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>

为RecyclerView设置LayoutManagerAdapterLayoutManager决定了RecyclerView中item的显示方式,Google提供了三种实现好了的LayoutManager,分别是LinearLayoutManager(线性)、GridLayoutManager(网格)、StaggeredGridLayoutManager(瀑布流)。这三种layoutManager几乎能满足大部分的日常需求,否则也可以通过继承LayoutManager自定义显示方式:

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
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private RvAdapter mAdapter;

private List<String> mDataList;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mRecyclerView = findViewById(R.id.rv);
initData();

//创建线性布局管理器
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(linearLayoutManager);
//创建Adapter
mAdapter = new RvAdapter(mDataList);
mRecyclerView.setAdapter(mAdapter);
}

private void initData() {
mDataList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
mDataList.add("" + i);
}
}
}

Adapter决定了RecyclerView以何种方式显示你的数据,使用时需要继承Adapter类,并重写其中的抽象方法。ViewHolder也是一个需要被继承的类,它用于保存你创建的ItemView

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
public class RvAdapter extends RecyclerView.Adapter<RvAdapter.MyViewHolder> {
private List<String> mDataList;

public RvAdapter(List<String> list) {
mDataList = new ArrayList<>();
mDataList.addAll(list);
}

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//在RecyclerView需要创建ViewHolder时此方法被回掉
//在此方法中加载你的ItemView并返回创建的ViewHolder
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.item_rv/*ItemView的xml*/, parent, false/*这里一定要是false*/);
return new MyViewHolder(itemView);
}

@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
//在RecyclerView需要显示指定位置的数据时此方法被回掉
//通常在这个方法中更新View
holder.mTextView.setText(mDataList.get(position));
//设置点击监听器(RecyclerView没有提供类似于ListView的OnItemClickListener)
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "Item " + position + " is clicked", Toast.LENGTH_LONG).show();
}
});
}

@Override
public int getItemCount() {
//返回item的数量
return mDataList.size();
}

public static class MyViewHolder extends RecyclerView.ViewHolder {
private TextView mTextView;

public MyViewHolder(View itemView) {
super(itemView);
mTextView = itemView.findViewById(R.id.tv);
}
}
}

ItemView的xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:gravity="center" />
</FrameLayout>

效果图:

分割线

有时候我们需要给每个item之间添加分割线,但是RecyclerView并没有为我们提供类似于divider这样的属性。那怎么添加分割线呢?通常有两种添加分割线的方法,最简单快捷的做法是在ItemView的底部添加一个高度为1dp的View:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:gravity="center" />

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#4e4e4e" />
</LinearLayout>

最后的效果:

第二种方法是通过ItemDecoration,这种方式比之前的方法要麻烦一点,后面再说。

下拉刷新

许多App的列表都有下拉刷新的功能,要实现这个其实很简单,官方为我们提供了SwipeRefreshLayout这个控件。首先在xml中用这个控件嵌套RecyclerView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jay86.rv.MainActivity">

<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v4.widget.SwipeRefreshLayout>

</FrameLayout>

SwipeRefreshLayout设置监听器(省略了不相关代码):

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
public class MainActivity extends AppCompatActivity {
private RvAdapter mAdapter;

private SwipeRefreshLayout mSwipeRefreshLayout;

private List<String> mDataList;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

... ...

mSwipeRefreshLayout = findViewById(R.id.refreshLayout);

... ...

mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//在用户做刷新操作时此方法被回掉
refreshData();
}
});
}

private void refreshData() {
//模拟刷新数据
new Thread(new Runnable() {
@Override
public void run() {
try {
mDataList.clear();
for (int i = 0; i < 20; i++) {
mDataList.add("" + (100 + i));
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
//关闭刷新动画
mSwipeRefreshLayout.setRefreshing(false);
//更新数据
mAdapter.refreshData(mDataList);
}
});
}
}).start();
}
}

SwipeRefreshLayout只提供了一个刷新的动画,刷新数据的操作需要我们自己去实现。为了更新数据,我们可以在Adapter中暴露一个刷新数据的方法(省略了不相关代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RvAdapter extends RecyclerView.Adapter<RvAdapter.MyViewHolder> {
private List<String> mDataList;

public RvAdapter(List<String> list) {
mDataList = new ArrayList<>();
mDataList.addAll(list);
}

... ...

public void refreshData(List<String> list) {
mDataList.clear();
mDataList.addAll(list);
//通知列表刷新
notifyDataSetChanged();
}

... ...
}

需要注意的是,每当Adapter中的数据改变,需要调用notifyDataSetChanged()方法通知RecyclerView更新列表。

效果如下:

上拉加载

如果数据太多,一般才用分批加载的策略,即首次只加载部分数据,当用户滑动到最后一个Item时再加载后面的数据。实现这个有个非常快捷的方法,只要在onBindViewHolder中判断一下位置即可:

1
2
3
4
5
6
7
8
9
10
11
... ...

@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
... ...

if (position == getItemCount() - 1) {
loadData();
}
}
... ...

ViewType

之前我们的列表每一个item都是一样,但是有时候我们需要在某个位置或多个位置放置不同的item。这时我们需要重写Adapter的getItemViewType()方法,并为每个item创建一个布局和ViewHolder。为了例子简单,我们只在头部添加一张图片:
Adapter:

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
public class RvAdapter extends RecyclerView.Adapter {
public static final int TYPE_HEADER = 0;
public static final int TYPE_NORMAL = 1;

private List<String> mDataList;

public RvAdapter(List<String> list) {
mDataList = new ArrayList<>();
mDataList.addAll(list);
}

@Override
public int getItemViewType(int position) {
//根据position判断View的类型,并返回不同的值
return position == 0 ? TYPE_HEADER : TYPE_NORMAL;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
//此处的viewType即为getItemViewType()方法的返回值
//根据不同的viewType返回不同的ViewHolder
if (viewType == TYPE_HEADER) {
View itemView = inflater.inflate(R.layout.item_header, parent, false);
return new HeaderViewHolder(itemView);
} else {
View itemView = inflater.inflate(R.layout.item_normal, parent, false);
return new NormalViewHolder(itemView);
}
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
//对不同的viewType做相应的处理
if (getItemViewType(position) == TYPE_HEADER) {
HeaderViewHolder header = (HeaderViewHolder) holder;
header.mImageView.setImageResource(R.drawable.ic_launcher_foreground);
header.mImageView.setBackgroundResource(R.drawable.ic_launcher_background);
} else {
NormalViewHolder normal = (NormalViewHolder) holder;
//添加了头部,获取mDataList里面的值时需要对position做处理
normal.mTextView.setText(mDataList.get(position - 1));
}
}

@Override
public int getItemCount() {
return mDataList.size() + 1/*多了header*/;
}

public static class HeaderViewHolder extends ViewHolder {
private ImageView mImageView;

public HeaderViewHolder(View itemView) {
super(itemView);
mImageView = itemView.findViewById(R.id.imageView);
}
}

public static class NormalViewHolder extends ViewHolder {
private TextView mTextView;

public NormalViewHolder(View itemView) {
super(itemView);
mTextView = itemView.findViewById(R.id.tv);
}
}
}

item_header.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop" />
</FrameLayout>

其他代码无变动,不做展示,最后的效果如下:

ItemDecoration

之前添加分割线时提到了ItemDecoration,但是ItemDecoration的作用远不止添加分割线这么简单,它还可以为每个item绘制装饰。官方没有为我们提供可用的ItemDecoration实现类。首先继承ItemDecoration类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
}

ItemDecoration提供的三个方法都不是抽象方法,可以根据需求重写其中的一个或多个方法。接下来就说一下这三个方法分别有什么作用。通过getItemOffsets()方法可以给ItemView设置偏移。看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//获取itemView(第二个参数)在Adapter中的位置
//注意是getChildAdapterPosition(view),不是getChildLayoutPosition(view)
int position = parent.getChildAdapterPosition(view);
//将dp转换成px(模板代码,熟记)
DisplayMetrics displayMetrics = view.getResources().getDisplayMetrics();
float horizontalMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP/*单位*/,
4 * position/*值*/, displayMetrics);
//设置偏移,四个参数从左到右分别为left, top, right, bottom
outRect.set((int) horizontalMargin, 0, (int) horizontalMargin, 0);
}
}

添加到RecyclerView:

1
2
3
... ...
mRecyclerView.addItemDecoration(new MyItemDecoration());
... ...

得到的效果如下(为了区分,将ItemView背景设置为了粉红色):

onDraw()onDrawOver()都是在ItemView上绘制装饰,它们的区别是:前者在ItemView绘制前被调用,后者在ItemView被绘制后调用,即onDrawOver()方法绘制的内容将显示在最上层,中间是ItemView,最下面是onDraw()方法绘制的内容。结合之前的方法,在item间绘制分割线作为例子:

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
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private Context mContext;
private int mDividerWidth;
private Paint mPaint;

public MyItemDecoration(Context context) {
mContext = context;
mDividerWidth = dp2px(1);

mPaint = new Paint(); //创建一个画笔
mPaint.setColor(Color.BLACK); //设置画笔颜色
mPaint.setAntiAlias(true); //设置抗锯齿
mPaint.setStrokeWidth(mDividerWidth); //设置画笔宽度
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//获取itemView(第二个参数)在Adapter中的位置
//注意是getChildAdapterPosition(view),不是getChildLayoutPosition(view)
int position = parent.getChildAdapterPosition(view);
//在两个item之间偏移1dp
if (position != parent.getAdapter().getItemCount() - 1) {
//设置偏移,四个参数从左到右分别为left, top, right, bottom
outRect.set(0, 0, 0, mDividerWidth);
}
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();

//为当前正在使用的itemView添加分割线
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
//获取itemView左下角坐标
int startX = child.getLeft();
int y = child.getBottom();
int endX = child.getRight();
//绘制直线
c.drawLine(startX, y, endX, y, mPaint);
}
}

private int dp2px(int dp) {
//将dp转换成px(模板代码,熟记)
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP/*单位*/,
dp/*值*/, displayMetrics);
}
}

效果如下:

因为是自己绘制的,所以你也可以绘制斜向的分割线,只要改一些直线的方向就好了:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyItemDecoration extends RecyclerView.ItemDecoration {
... ...

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
... ...
c.drawLine(startX, y, endX, y + dp2px(20), mPaint);
... ...
}
... ...
}

效果如下:

分类标签

有些时候我们需要在item中插入分类标签,如果用viewType来处理是比较麻烦的,但是如果用之前提到的ItemDecoration来处理就简单多了:

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
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private Context mContext;
private List<String> mDataList;
private int mOffset;
private int mTextSize;
private Paint mPaint;

public MyItemDecoration(Context context, RvAdapter adapter) {
mContext = context;
mDataList = adapter.getDataList();

mOffset = dp2px(16);
mTextSize = sp2px(16);

mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//注意是getChildAdapterPosition(view),不是getChildLayoutPosition(view)
int position = parent.getChildAdapterPosition(view);
//为了例子简单,这里以数字的十位作为分组依据
//在每一组的第一个item前留出间距
if (isFirstInGroup(position)) {
outRect.set(0, mOffset, 0, 0);
}
}

@Override
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);

int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
//注意是getChildAdapterPosition(view),不是getChildLayoutPosition(view)
int position = parent.getChildAdapterPosition(child);
if (isFirstInGroup(position)) {
drawTag(child, canvas, mDataList.get(position) + "+");
}
}
}

private void drawTag(View child, Canvas canvas, String tag) {
int x = child.getWidth() / 2;
int y = child.getTop();
canvas.drawText(tag, x, y, mPaint);
}

private boolean isFirstInGroup(int position) {
if (position == 0) {
return true;
} else {
int cur = Integer.parseInt(mDataList.get(position));
int pre = Integer.parseInt(mDataList.get(position - 1));
return cur / 10 != pre / 10;
}
}

private int dp2px(int dp) {
//将dp转换成px(模板代码,熟记)
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP/*单位*/,
dp/*值*/, displayMetrics);
}

private int sp2px(int sp) {
//将dp转换成px(模板代码,熟记)
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP/*单位*/,
sp/*值*/, displayMetrics);
}
}

效果如下:

标签悬浮

在某些App上我们还能看到悬浮标签的效果,这个同样也可以通过ItemDecoration来实现:

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
100
101
102
103
104
105
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private Context mContext;
private List<String> mDataList;
private int mOffset;
private int mTextSize;
private Paint mPaint;

public MyItemDecoration(Context context, RvAdapter adapter) {
mContext = context;
mDataList = adapter.getDataList();

mOffset = dp2px(24);
mTextSize = sp2px(16);

mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//注意是getChildAdapterPosition(view),不是getChildLayoutPosition(view)
int position = parent.getChildAdapterPosition(view);
//为了例子简单,这里以数字的十位作为分组依据
//在每一组的第一个item前留出间距
if (isFirstInGroup(position)) {
outRect.set(0, mOffset, 0, 0);
}
}

@Override
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);

int itemCount = state.getItemCount();
int childCount = parent.getChildCount();
int left = parent.getLeft();
int right = parent.getRight();
String preTag;
String curTag = null;
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
preTag = curTag;
curTag = getTag(position);
if (Objects.equals(curTag, preTag))
continue;
int bottom = child.getBottom();
int top = Math.max(mOffset, child.getTop());
if (position + 1 < itemCount) {
String nextTag = getTag(position + 1);
if (!curTag.equals(nextTag) && bottom < top) {
top = bottom;
}
}
drawTag(left, top - mOffset, right, top, canvas, curTag);
}
}

private void drawTag(int left, int top, int right, int bottom, Canvas canvas, String tag) {
//绘制背景
mPaint.setColor(Color.BLACK);
canvas.drawRect(left, top, right, bottom, mPaint);

//绘制文字
mPaint.setColor(Color.WHITE);
Rect rect = new Rect();
mPaint.getTextBounds(tag, 0, tag.length(), rect);
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int x = (left + right - rect.width()) / 2;
int y = (bottom + top - fontMetrics.bottom - fontMetrics.top) / 2;
canvas.drawText(tag, x, y, mPaint);
}

private boolean isFirstInGroup(int position) {
if (position == 0) {
return true;
} else {
int cur = Integer.parseInt(mDataList.get(position));
int pre = Integer.parseInt(mDataList.get(position - 1));
return cur / 10 != pre / 10;
}
}

private String getTag(int position) {
int num = Integer.parseInt(mDataList.get(position));
return num / 10 * 10 + "+";
}

private int dp2px(int dp) {
//将dp转换成px(模板代码,熟记)
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP/*单位*/,
dp/*值*/, displayMetrics);
}

private int sp2px(int sp) {
//将dp转换成px(模板代码,熟记)
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP/*单位*/,
sp/*值*/, displayMetrics);
}
}

效果如下:

拖拽与滑动

官方提供了ItemTouchHelper类来帮助处理RecyclerView的item滑动与拖拽操作,使用方法同样非常简单,只要以下几个步骤:

  • 创建ItemTouchHelper对象;
  • 实现ItemTouchHelper.Callback(官方提供了它的简化版本ItemTouchHelper.SimpleCallback);
  • 绑定RecyclerView。
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
public class MainActivity extends AppCompatActivity {

... ...

@Override
protected void onCreate(Bundle savedInstanceState) {

... ...

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN/*拖拽方向*/, ItemTouchHelper.LEFT/*滑动方向*/
) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//长按拖拽item时此方法被回掉
int from = viewHolder.getAdapterPosition(); //获取开始位置
int to = target.getAdapterPosition(); //获取结束位置

//进行数据交换
List<String> dataList = mAdapter.getDataList();
if (from < to) {
for (int i = from; i < to; i++) {
Collections.swap(dataList, i, i + 1);
}
} else {
for (int i = from; i > to; i--) {
Collections.swap(dataList, i, i - 1);
}
}
//通知item被移动
mAdapter.notifyItemMoved(from, to);
return true;
}

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//滑动item时此方法被回掉
int position = viewHolder.getAdapterPosition(); //获取位置
List<String> dataList = mAdapter.getDataList();
dataList.remove(position); //删除数据
mAdapter.notifyItemRemoved(position); //通知item被删除
}
});
//绑定RecyclerView
itemTouchHelper.attachToRecyclerView(mRecyclerView);
}
}

效果如下:

除了最基本的这些,通过重写ItemTouchHelper.Callback中的方法还能实现许多的效果。例如重写其中的onChildDraw()方法实现滑动删除时使item慢慢透明的效果:

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
public class MainActivity extends AppCompatActivity {

... ...

@Override
protected void onCreate(Bundle savedInstanceState) {

... ...

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN/*拖拽方向*/, ItemTouchHelper.LEFT/*滑动方向*/
) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//长按拖拽item时此方法被回掉
int from = viewHolder.getAdapterPosition(); //获取开始位置
int to = target.getAdapterPosition(); //获取结束位置

//进行数据交换
List<String> dataList = mAdapter.getDataList();
if (from < to) {
for (int i = from; i < to; i++) {
Collections.swap(dataList, i, i + 1);
}
} else {
for (int i = from; i > to; i--) {
Collections.swap(dataList, i, i - 1);
}
}
//通知item被移动
mAdapter.notifyItemMoved(from, to);
return true;
}

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//滑动item时此方法被回掉
int position = viewHolder.getAdapterPosition(); //获取位置
List<String> dataList = mAdapter.getDataList();
dataList.remove(position); //删除数据
mAdapter.notifyItemRemoved(position); //通知item被删除
}

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
//滑动时改变Item的透明度
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float alpha = 1 - Math.abs(1.0f * dX / viewHolder.itemView.getWidth());
viewHolder.itemView.setAlpha(alpha);
}
}
});
//绑定RecyclerView
itemTouchHelper.attachToRecyclerView(mRecyclerView);
}
}

效果如下:

ItemAnimator

对于ItemAnimator,官方是这样解释的:

This class defines the animations that take place on items as changes are made to the adapter.

暂时不想写例子了。。。