如何给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; } 
这种方式的最大好处就是只需要为整个列表创建一个 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)  {                }     }) ); 
而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) {} } 
这个方案的有如下好处:
只需要为整个列表创建一个点击事件监听器对象 
通过 position 参数可以识别到点击事件来源 
除了监听点击事件,同时也能监听长按事件 
 
问题 最开始的需求如下面的截图,它只是一个好友列表。点击列表中的每一个 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 不太好实现,因为它对应的是整个 item 的点击事件。而上图中 viewType 为 1 和 2 的 item 中有 Button,我们自然希望是点击 Button 控件区域才算真正的点击,否则用户体验有些怪。 
没办法,所以还是得乖乖给需要点击事件的 Button 控件添加相应的 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给拦截了。这个帖子 中有两个图片很好地展示了 touch 事件分发和处理过程,对照这两个图我们不难理解问题所在。
分析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事件。问题完美解决。