RecyclerView.OnItemTouchListener的用法
如何给RecyclerView
中的Item添加点击事件呢?本文介绍几种不同的实现方法。
前言
RecyclverView的item上添加点击事件响应的最简单做法是给item的view添加OnClickListener监听器。比如:1
2
3
4
5
6
7
8public void onBindViewHolder(final ViewHolder holder, int position) {
holder.rlRoot.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// here is your code
}
});
}
这篇帖子认为虽然这种做法并无严重不妥,但每次绑定数据时就重新创建一个listener总觉不是很自然。对此我也深表认同。
那有没有更好的办法呢?比如说类似ListView的OnItemClickListener,用起来方便很多。
方法一
一种建议的做法是从Fragment或Activity中向Adapter传一个listener。示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// Activity的代码
private View.OnClickListener mItemClick = new View.OnClickListener() {
public void onClick(View v) {
MainActivity.start();
}
};
MainAdapter mainAdapter = new MainAdapter(this, mItemClick);
// Adapter的代码
public MainAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
View itemView = activity.getLayoutInflater().inflate(R.layout.main_adapter_item, viewGroup, false);
ViewHolder holder = new ViewHolder(itemView);
itemView.setOnClickListener(mItemClick);
return holder;
}
方法二
这里给出了一种更好的做法。1
2
3
4
5
6
7
8
9
10
11
12RecyclerView recyclerView = findViewById(R.id.recycler);
recyclerView.addOnItemTouchListener(
new RecyclerItemClickListener(context, recyclerView ,new RecyclerItemClickListener.OnItemClickListener() {
public void onItemClick(View view, int position) {
// do whatever
}
public void onLongItemClick(View view, int position) {
// do whatever
}
})
);
而RecyclerItemClickListener
的实现如下: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
51public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener {
private OnItemClickListener mListener;
public interface OnItemClickListener {
public void onItemClick(View view, int position);
public void onLongItemClick(View view, int position);
}
GestureDetector mGestureDetector;
public RecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
public boolean onSingleTapUp(MotionEvent e) {
if (mChildView != null && mListener != null) {
final int pos = mRecyclerView.getChildAdapterPosition(mChildView);
if (pos != RecyclerView.NO_POSITION) {
mListener.onItemClick(mChildView, pos);
return true;
}
}
return false;
}
public void onLongPress(MotionEvent e) {
View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (child != null && mListener != null) {
mListener.onLongItemClick(child, recyclerView.getChildAdapterPosition(child));
}
}
});
}
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View childView = view.findChildViewUnder(e.getX(), e.getY());
if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
mListener.onItemClick(childView, view.getChildAdapterPosition(childView));
return true;
}
return false;
}
public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
public void onRequestDisallowInterceptTouchEvent (boolean disallowIntercept){}
}
问题
最开始的需求如下面的截图,它只是一个好友列表。点击列表中的每一个item要求跳转到个人主页。可以看到,这里非常适合使用上述的RecyclerItemClickListener方案,可以避免每次绑定数据时创建listener的问题,简直跟ListView的OnItemClickListener一样好用。
1
2
3
4
5
6
7
8
9
10
11
12RecyclerView recyclerView = findViewById(R.id.recycler);
recyclerView.addOnItemTouchListener(
new RecyclerItemClickListener(context, recyclerView ,new RecyclerItemClickListener.OnItemClickListener() {
public void onItemClick(View view, int position) {
// 跳转到个人主页
}
public void onLongItemClick(View view, int position) {
// do whatever
}
})
);
一开始上面的做法的确非常不错。不幸的是,最近需求发生了变化。该页面除了是好友列表,还要求在某些条件下会出来以下两种情形。
注意:这里的绿色方框中的内容是由RecyclerView的特殊item来实现的。
- 第一个截图,点击红色框中的按钮跳转到加”绑定手机”页面
- 第二个截图,点击绿色框中的用户信息区域跳转到个人主页,点击红色框中的按钮跳转”加好友”
使用RecyclerItemClickListener不太好实现,看来它有其局限性,所以还是乖乖给需要点击事件的控件添加相应的OnClickListener吧。给”绑定手机”,”加好友”,以及用户区域添加OnClickListener后,问题来了:
- 第一个截图中,点击”绑定手机”不起作用,双击倒是可以跳转到加好友页面
- 第二个截图中,点击绿色方框中的用户区域不起作用,点击”加好友”也不起作用
怎么办?完全去掉RecyclerItemClickListener
,然后回退到给需要点击事件的控件逐个添加OnClickListener?不甘心啊不甘心(关键是不想大改)。
于是又作了如下尝试:1
2
3
4
5
6
7
8
9
10
11
12RecyclerView recyclerView = findViewById(R.id.recycler);
recyclerView.addOnItemTouchListener(
new RecyclerItemClickListener(context, recyclerView ,new RecyclerItemClickListener.OnItemClickListener() {
public void onItemClick(View view, int position) {
// 在这里进行处理
}
public void onLongItemClick(View view, int position) {
// do whatever
}
})
);
- 第一个截图中,点击绿色方框部分任何一处,均可跳转到加好友页面
- 第二个截图中,点击绿色方框中的用户区域任何一处,均可跳转到加好友页面,点击”加好友”不起作用
思考
为什么上述场景中点击事件不起任何作用?
先来看一下RecyclerView.OnItemTouchListener
。OnItemTouchListener允许应用拦截RecyclerView的view hierarchy level处理过程中的触摸事件,以防这些事件被认为是RecyclerView的滚动操作。
再来看RecyclerView.addOnItemTouchListener()
。该方法用于添加RecyclerView.OnItemTouchListener,在touch事件被分发到child view或被用作view的标准滚动行为之前拦截这些事件。客户端代码可以使用监听器来实现item的操作行为。一旦从RecyclerView.OnItemTouchListener.onInterceptTouchEvent(RecyclerView, MotionEvent)
返回true,则对应的RecyclerView.OnItemTouchListener.onTouchEvent(RecyclerView, MotionEvent)
方法会在收到每个MotionEvent事件时被调用,直到手势结束。
所以很明显,viewHolder.button.setOnClickListener
这种方式设置的监听器不起作用是因为touch事件被OnItemTouchListener给拦截了。
分析RecyclerItemClickListener的实现,不难找到问题的关键在于:如果找到了mChildView在RecyclerView中的位置,会直接调用mListener.onItemClick()并返回true。而返回true意味着touch事件被消费掉了,再也没可能被分发到child view。所以类似viewHolder.button.setOnClickListener
这种方式设置的监听器必然不起作用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener {
public RecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
public boolean onSingleTapUp(MotionEvent e) {
if (mChildView != null && mListener != null) {
final int pos = mRecyclerView.getChildAdapterPosition(mChildView);
if (pos != RecyclerView.NO_POSITION) {
mListener.onItemClick(mChildView, pos);
return true;
}
}
return false;
}
...
});
}
如何修复这里的问题?暂时想不到比较通用的解决方案,但对于我们这里的问题还是不难处理的。解决办法就是针对那些特殊viewType对应的item,保证一定不会消费其touch事件,而是允许分发出去让child view处理。修改后的代码如下: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
33private List<Integer> mSpecialViewTypes;
public void setSpecialViewTypes(List<Integer> specialViewTypes) {
mSpecialViewTypes = specialViewTypes;
}
private List<Integer> mSpecialViewTypes;
public RecyclerItemClickListener(Context context, OnItemClickListener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
public boolean onSingleTapUp(MotionEvent e) {
if (mChildView != null && mListener != null) {
final int pos = mRecyclerView.getChildAdapterPosition(mChildView);
//========================================
// 对于特殊的viewType, onSingleTapUp()返回false
int viewType = mRecyclerView.getAdapter().getItemViewType(pos);
if (mSpecialViewTypes != null && mSpecialViewTypes.contains(viewType)) {
return false;
}
//========================================
if (pos != RecyclerView.NO_POSITION) {
mListener.onItemClick(mChildView, pos);
return true;
}
}
return false;
}
}
}
具体到我们的例子,view type 1
和view type 2
就是所谓的特殊viewType,我们不应在onSingleTapUp()
方法中拦截那些会分发到它们的touch事件。修改后问题完美解决。