如何给RecyclerView
中的Item添加点击事件呢?本文介绍几种不同的实现方法。
前言 RecyclverView的item上添加点击事件响应的最简单做法是给item的view添加OnClickListener监听器。比如:
1 2 3 4 5 6 7 8 public void onBindViewHolder (final ViewHolder holder, int position) { holder.rlRoot.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { } }); }
这篇帖子 认为虽然这种做法并无严重不妥,但每次绑定数据时就重新创建一个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 private View.OnClickListener mItemClick = new View .OnClickListener() { @Override public void onClick (View v) { MainActivity.start(); } }; MainAdapter mainAdapter = new MainAdapter (this , mItemClick); @Override 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 12 RecyclerView recyclerView = findViewById(R.id.recycler);recyclerView.addOnItemTouchListener( new RecyclerItemClickListener (context, recyclerView ,new RecyclerItemClickListener .OnItemClickListener() { @Override public void onItemClick (View view, int position) { } @Override public void onLongItemClick (View view, int position) { } }) );
而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 51 public 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() { @Override 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 ; } @Override public void onLongPress (MotionEvent e) { View child = recyclerView.findChildViewUnder(e.getX(), e.getY()); if (child != null && mListener != null ) { mListener.onLongItemClick(child, recyclerView.getChildAdapterPosition(child)); } } }); } @Override 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 ; } @Override public void onTouchEvent (RecyclerView view, MotionEvent motionEvent) { } @Override public void onRequestDisallowInterceptTouchEvent (boolean disallowIntercept) {} }
问题 最开始的需求如下面的截图,它只是一个好友列表。点击列表中的每一个item要求跳转到个人主页。可以看到,这里非常适合使用上述的RecyclerItemClickListener方案,可以避免每次绑定数据时创建listener的问题,简直跟ListView的OnItemClickListener一样好用。
1 2 3 4 5 6 7 8 9 10 11 12 RecyclerView recyclerView = findViewById(R.id.recycler);recyclerView.addOnItemTouchListener( new RecyclerItemClickListener (context, recyclerView ,new RecyclerItemClickListener .OnItemClickListener() { @Override public void onItemClick (View view, int position) { } @Override public void onLongItemClick (View view, int position) { } }) );
一开始上面的做法的确非常不错。不幸的是,最近需求发生了变化。该页面除了是好友列表,还要求在某些条件下会出来以下两种情形。
注意:这里的绿色方框中的内容是由RecyclerView的特殊item来实现的。
第一个截图,点击红色框中的按钮跳转到加”绑定手机”页面
第二个截图,点击绿色框中的用户信息区域跳转到个人主页,点击红色框中的按钮跳转”加好友”
使用RecyclerItemClickListener不太好实现,看来它有其局限性,所以还是乖乖给需要点击事件的控件添加相应的OnClickListener吧。给”绑定手机”,”加好友”,以及用户区域添加OnClickListener后,问题来了:
第一个截图中,点击”绑定手机”不起作用,双击倒是可以跳转到加好友页面
第二个截图中,点击绿色方框中的用户区域不起作用,点击”加好友”也不起作用
怎么办?完全去掉RecyclerItemClickListener
,然后回退到给需要点击事件的控件逐个添加OnClickListener?不甘心啊不甘心(关键是不想大改)。
于是又作了如下尝试:
1 2 3 4 5 6 7 8 9 10 11 12 RecyclerView recyclerView = findViewById(R.id.recycler);recyclerView.addOnItemTouchListener( new RecyclerItemClickListener (context, recyclerView ,new RecyclerItemClickListener .OnItemClickListener() { @Override public void onItemClick (View view, int position) { } @Override public void onLongItemClick (View view, int position) { } }) );
第一个截图中,点击绿色方框部分任何一处,均可跳转到加好友页面
第二个截图中,点击绿色方框中的用户区域任何一处,均可跳转到加好友页面,点击”加好友”不起作用
思考 为什么上述场景中点击事件不起任何作用?
先来看一下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 21 public class RecyclerItemClickListener implements RecyclerView .OnItemTouchListener { public RecyclerItemClickListener (Context context, final RecyclerView recyclerView, OnItemClickListener listener) { mListener = listener; mGestureDetector = new GestureDetector (context, new GestureDetector .SimpleOnGestureListener() { @Override 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 33 private 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() { @Override public boolean onSingleTapUp (MotionEvent e) { if (mChildView != null && mListener != null ) { final int pos = mRecyclerView.getChildAdapterPosition(mChildView); 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事件。修改后问题完美解决。