目录

BaseRecyclerViewAdapterHelper开源项目之点击事件源码学习

目录

##version:2.8.5

今天我们主要来分析BaseRecyclerViewAdapterHelper为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
 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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
 public abstract class SimpleClickListener implements RecyclerView.OnItemTouchListener {
private GestureDetectorCompat mGestureDetector;
private RecyclerView recyclerView;
protected BaseQuickAdapter baseQuickAdapter;
public static String TAG = "SimpleClickListener";
private boolean mIsPrepressed = false;
private boolean mIsShowPress = false;
private View mPressedView = null;

@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
    Log.i(TAG,">>>>onInterceptTouchEvent e"+e.getActionMasked());
    if (recyclerView == null) {
        this.recyclerView = rv;
        this.baseQuickAdapter = (BaseQuickAdapter) recyclerView.getAdapter();
        mGestureDetector = new GestureDetectorCompat(recyclerView.getContext(), new ItemTouchHelperGestureListener(recyclerView));
    }else if (recyclerView!=rv){
        this.recyclerView = rv;
        this.baseQuickAdapter = (BaseQuickAdapter) recyclerView.getAdapter();
        mGestureDetector = new GestureDetectorCompat(recyclerView.getContext(), new ItemTouchHelperGestureListener(recyclerView));
    }
    if (!mGestureDetector.onTouchEvent(e) && e.getActionMasked() == MotionEvent.ACTION_UP && mIsShowPress) {
        if (mPressedView!=null){
            BaseViewHolder vh = (BaseViewHolder) recyclerView.getChildViewHolder(mPressedView);
            if (vh == null ||!isHeaderOrFooterView(vh.getItemViewType())) {
                mPressedView.setPressed(false);
            }
        }
        mIsShowPress = false;
        mIsPrepressed = false;
    }
    return false;
}

@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    Log.i(TAG,">>>>onTouchEvent e"+e.getActionMasked());
    mGestureDetector.onTouchEvent(e);
}

@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    Log.i(TAG,">>>>onRequestDisallowInterceptTouchEvent disallowIntercept"+disallowIntercept);
}

private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {

    private RecyclerView recyclerView;

    ItemTouchHelperGestureListener(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
    }
    @Override
    public boolean onDown(MotionEvent e) {
        mIsPrepressed = true;
        mPressedView = recyclerView.findChildViewUnder(e.getX(), e.getY());
        super.onDown(e);
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
        Log.i(TAG,">>>>onShowPress e"+e);
        if (mIsPrepressed && mPressedView != null) {
    //    mPressedView.setPressed(true);
            mIsShowPress = true;
        }
        super.onShowPress(e);
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Log.i(TAG,">>>>onSingleTapUp e"+e);
        if (mIsPrepressed && mPressedView != null) {
            if (recyclerView.getScrollState()!=RecyclerView.SCROLL_STATE_IDLE){
                return false;
            }
            final View pressedView = mPressedView;
            BaseViewHolder vh = (BaseViewHolder) recyclerView.getChildViewHolder(pressedView);

            if (isHeaderOrFooterPosition(vh.getLayoutPosition())) {
                return false;
            }
            Set childClickViewIds = vh.getChildClickViewIds();
            Set nestViewIds = vh.getNestViews();
            if (childClickViewIds != null && childClickViewIds.size() > 0) {
                for (Integer childClickViewId : childClickViewIds) {
                    View childView = pressedView.findViewById(childClickViewId);
                    if (childView != null) {
                        if (inRangeOfView(childView, e) && childView.isEnabled()) {

                            if (nestViewIds!=null&&nestViewIds.contains(childClickViewId)){
                                return false;
                            }

                            setPressViewHotSpot(e, childView);
                            childView.setPressed(true);
                            onItemChildClick(baseQuickAdapter, childView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
                            resetPressedView(childView);
                            return true;
                        } else {
                            childView.setPressed(false);
                        }
                    }

                }
                setPressViewHotSpot(e,pressedView);
                mPressedView.setPressed(true);
                for (Integer childClickViewId : childClickViewIds) {
                    View childView = pressedView.findViewById(childClickViewId);
                    if (childView!=null){
                        childView.setPressed(false);
                    }
                }
                onItemClick(baseQuickAdapter, pressedView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
            } else {
                setPressViewHotSpot(e,pressedView);
                mPressedView.setPressed(true);
                if (childClickViewIds != null && childClickViewIds.size() > 0) {
                    for (Integer childClickViewId : childClickViewIds) {
                        View childView = pressedView.findViewById(childClickViewId);
                        if (childView!=null){
                            childView.setPressed(false);
                        }
                    }
                }

                onItemClick(baseQuickAdapter, pressedView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
            }
            resetPressedView(pressedView);

        }
        return true;
    }

    private void resetPressedView(final View pressedView) {
        if (pressedView!=null){
            pressedView.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (pressedView!=null){
                        pressedView.setPressed(false);
                    }

                }
            }, 100);
        }

        mIsPrepressed = false;
        mPressedView = null;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        Log.i(TAG,">>>>onLongPress e"+e);
        boolean isChildLongClick =false;
        if (recyclerView.getScrollState()!=RecyclerView.SCROLL_STATE_IDLE){
            return ;
        }
        if (mIsPrepressed && mPressedView != null) {
            mPressedView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            BaseViewHolder vh = (BaseViewHolder) recyclerView.getChildViewHolder(mPressedView);
            if (!isHeaderOrFooterPosition(vh.getLayoutPosition())) {
                Set longClickViewIds = vh.getItemChildLongClickViewIds();
                Set nestViewIds = vh.getNestViews();
                if (longClickViewIds != null && longClickViewIds.size() > 0) {
                    for (Integer longClickViewId : longClickViewIds) {
                        View childView = mPressedView.findViewById(longClickViewId);
                        if (inRangeOfView(childView, e) && childView.isEnabled()) {
                            if (nestViewIds!=null&&nestViewIds.contains(longClickViewId)){
                                isChildLongClick=true;
                                break;
                            }
                            setPressViewHotSpot(e, childView);
                            onItemChildLongClick(baseQuickAdapter, childView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
                            childView.setPressed(true);
                            mIsShowPress = true;
                            isChildLongClick = true;
                            break;
                        }
                    }
                }
                if (!isChildLongClick){

                    onItemLongClick(baseQuickAdapter, mPressedView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
                    setPressViewHotSpot(e,mPressedView);
                    mPressedView.setPressed(true);
                    if (longClickViewIds != null) {
                        for (Integer longClickViewId : longClickViewIds) {
                            View childView = mPressedView.findViewById(longClickViewId);
                            childView.setPressed(false);
                        }
                    }
                    mIsShowPress = true;
                }

            }

        }
    }

}

private void setPressViewHotSpot(final MotionEvent e,final  View mPressedView) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        /**
         * when   click   Outside the region  ,mPressedView is null
         */
        if (mPressedView !=null && mPressedView.getBackground() != null) {
            mPressedView.getBackground().setHotspot(e.getRawX(), e.getY()-mPressedView.getY());
        }
    }
}

/**
 * Callback method to be invoked when an item in this AdapterView has
 * been clicked.
 *
 * @param view     The view within the AdapterView that was clicked (this
 *                 will be a view provided by the adapter)
 * @param position The position of the view in the adapter.
 */
public abstract void onItemClick(BaseQuickAdapter adapter, View view, int position);

/**
 * callback method to be invoked when an item in this view has been
 * click and held
 *
 * @param view     The view whihin the AbsListView that was clicked
 * @param position The position of the view int the adapter
 * @return true if the callback consumed the long click ,false otherwise
 */
public abstract void onItemLongClick(BaseQuickAdapter adapter, View view, int position);

public abstract void onItemChildClick(BaseQuickAdapter adapter, View view, int position);

public abstract void onItemChildLongClick(BaseQuickAdapter adapter, View view, int position);

public boolean inRangeOfView(View view, MotionEvent ev) {
    int[] location = new int[2];
    if (view==null||!view.isShown()){
        return false;
    }
    view.getLocationOnScreen(location);
    int x = location[0];
    int y = location[1];
    if (ev.getRawX() < x
            || ev.getRawX() > (x + view.getWidth())
            || ev.getRawY() < y
            || ev.getRawY() > (y + view.getHeight())) {
        return false;
    }
    return true;
}

private boolean isHeaderOrFooterPosition(int position) {
    /**
     *  have a headview and EMPTY_VIEW FOOTER_VIEW LOADING_VIEW
     */
    if (baseQuickAdapter==null){
        if (recyclerView!=null){
            baseQuickAdapter= (BaseQuickAdapter) recyclerView.getAdapter();
        }else {
            return false;
        }
    }
    int type = baseQuickAdapter.getItemViewType(position);
    return (type == EMPTY_VIEW || type == HEADER_VIEW || type == FOOTER_VIEW || type == LOADING_VIEW);
}
private boolean isHeaderOrFooterView(int type) {

    return (type == EMPTY_VIEW || type == HEADER_VIEW || type == FOOTER_VIEW || type == LOADING_VIEW);
}}

用过BVAH框架中的点击事件的小伙伴们应该对以下几个接口类不陌生:

  • OnItemChildClickListener 给childView添加点击事件监听器
  • OnItemChildLongClickListener 给childView添加长点击事件监听器
  • OnItemClickListener 给整个Item添加点击事件监听器
  • OnItemLongClickListener 给整个Item添加长点击事件监听器

里面都提供了响应的回调接口,但是赋予他们能力的其实是SimpleClickListener 这个类。是不是开始开兴趣了?让我们一起来看下这个类的真实面目:

1
public abstract class SimpleClickListener implements RecyclerView.OnItemTouchListener {....}
  • 首先我们发现SimpleClickListener 实现了RecyclerView.OnItemTouchListener 接口OnItemTouchListener接口是recyclerview提供的一个监听item被点击的监听器,源码如下:

    1
    2
    3
    4
    5
    6
    
    允许应用去拦截处理touch事件,通常实现操作recyclerview 的交互事件时用到
    public static interface OnItemTouchListener {
    在onInterceptTouchEvent方法会在recyclerview的childview 发生touch交互前被调用
      public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
      public void onTouchEvent(RecyclerView rv, MotionEvent e);
      public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept); }
    

注:点击事件的实现有多重方式,如:

  • 使用 RecyclerView提供的 addOnItemTouchListener()实现
  • 创建 ItemView时添加点击事件监听
  • 在 ItemView attach RecyclerView时实现

####类内字段解析

  • private GestureDetectorCompat mGestureDetector; 手势检测辅助类,相对于GestureDetectorCompat 有更好的兼容性,且api使用相同。
  • private RecyclerView recyclerView; 存储recyclerview实例对象,后面在获取adapter和viewholder时用到
  • protected BaseQuickAdapter baseQuickAdapter;
  • private boolean mIsPrepressed = false;控件被按下标标识
  • private boolean mIsShowPress = false; 控件press状态标识
  • private View mPressedView = null; 被点击的控件

接下来我们根据事件的传递机制进行分析,当touch事件发生时: onInterceptTouchEvent是第一个被调用的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
    Log.i(TAG,">>>>onInterceptTouchEvent e"+e.getActionMasked());
    if (recyclerView == null) {
        this.recyclerView = rv;
        this.baseQuickAdapter = (BaseQuickAdapter) recyclerView.getAdapter();
        mGestureDetector = new GestureDetectorCompat(recyclerView.getContext(), new ItemTouchHelperGestureListener(recyclerView));
    }else if (recyclerView!=rv){
        this.recyclerView = rv;
        this.baseQuickAdapter = (BaseQuickAdapter) recyclerView.getAdapter();
        mGestureDetector = new GestureDetectorCompat(recyclerView.getContext(), new ItemTouchHelperGestureListener(recyclerView));
    }
    if (!mGestureDetector.onTouchEvent(e) && e.getActionMasked() == MotionEvent.ACTION_UP && mIsShowPress) {
        if (mPressedView!=null){
            BaseViewHolder vh = (BaseViewHolder) recyclerView.getChildViewHolder(mPressedView);
            if (vh == null ||!isHeaderOrFooterView(vh.getItemViewType())) {
                mPressedView.setPressed(false);
            }
        }
        mIsShowPress = false;
        mIsPrepressed = false;
    }
    return false;
}

源码中,在onInterceptTouchEvent 方法中主要做了两件事情:

  • 初始化recyclerView、baseQuickAdapter、mGestureDetector实例
  • 如果点击事件最终没被消费掉,则恢复被touch而进入press状态中的控件状态。

然后我们就可以在onTouchEvent 方法中将touch事件交给mGestureDetector进行处理了

1
2
3
4
5
  @Override
   public void onTouchEvent(RecyclerView rv, MotionEvent e) {
       Log.i(TAG,">>>>onTouchEvent e"+e.getActionMasked());
       mGestureDetector.onTouchEvent(e);
   }

然后我们看在初始化mGestureDetector时绑定的ItemTouchHelperGestureListener 监听器, ItemTouchHelperGestureListener 监听器继承自SimpleOnGestureListener类,我们进入SimpleOnGestureListener的源码可以看到:

1
2
 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
        OnContextClickListener {

其内部其实是实现了在个手势交互监听器,只是方法体什么都没做,这样的好处是,我们继承他,可以不用实现一堆方法,需要用到哪个方法,重写该方法就可以了。

我们这次主要用到了如下几个方法:

  • onDown 按下时触发
  • onShowPress 处于press状态时触发
  • onSingleTapUp 单击事件触发
  • onLongPress 长点击事件press状态触发

可以看到,使用GestureDetectorCompat的好处。

根据一个touch事件的发生过程,down->showpress->up 即压下,显示挤压效果,松开弹起。

1
2
3
4
5
6
7
 @Override
    public boolean onDown(MotionEvent e) {
        mIsPrepressed = true;
        mPressedView = recyclerView.findChildViewUnder(e.getX(), e.getY());
        super.onDown(e);
        return false;
    }

在down中:

  • 重置mIsPrepressed 为true,当前已被点击,未松开

  • 根据e的x、y,获得被点击的view。

    @Override

    1
    2
    3
    4
    5
    6
    
      public void onShowPress(MotionEvent e) {
          if (mIsPrepressed && mPressedView != null) {
              mIsShowPress = true;
          }
          super.onShowPress(e);
      }
    

    在showpress中:

  • 根据down的结果,设置mIsShowPress字段,确定当前是否出于showpress状态

如果是单机事件则会触发 onSingleTapUp(MotionEvent e) 在从onSIngleTapUp的源码中可以看到:

1
if (mIsPrepressed && mPressedView != null) {
  • 如果是我们的recyclerview中的view被点击了才会执行后面的代码

    1
    2
    3
    
    if (recyclerView.getScrollState()!=RecyclerView.SCROLL_STATE_IDLE){
                  return false;
              }
    
  • 如果未处于RecyclerView.SCROLL_STATE_IDLE状态直接返回false,此时用户可能是在滑动,没不是点击操作

    1
    2
    3
    4
    5
    6
    
    final View pressedView = mPressedView;
              BaseViewHolder vh = (BaseViewHolder) recyclerView.getChildViewHolder(pressedView);
    
              if (isHeaderOrFooterPosition(vh.getLayoutPosition())) {
                  return false;
              }
    
  • 获取viewholder 如果是头部视图或者尾部视图,返回false ,我们不需要处理

接下来到了关键代码了:

1
 Set childClickViewIds = vh.getChildClickViewIds();
  • 获取添加了点击事件的view 的ids

    1
    
    Set nestViewIds = vh.getNestViews();     
    
  • 获取内嵌了recyclerview的view容器 的ids

    1
    
      if (childClickViewIds != null && childClickViewIds.size() > 0) {
    
  • 如果有view添加了点击事件监听

    1
    
     for (Integer childClickViewId : childClickViewIds) {      
    
  • 则开始遍历views

    1
    
    View childView = pressedView.findViewById(childClickViewId);
    
  • 根据ids获得添加了点击事件的view

    1
    
       if (inRangeOfView(childView, e) && childView.isEnabled()) {
    
  • 判断view处于点击范围内切isEnable

如果在点击范围内切isEnable = true则执行下面代码

1
2
    if (nestViewIds!=null&&nestViewIds.contains(childClickViewId)){
                return false;  }
  • 如果是内嵌recyclerview ,则不处理,交由子recyclerview进行处理。

经过层层过滤之后,剩下的就是当view被点击时需要做的操作了: 如果

  • setPressViewHotSpot(e, childView); 修改childview的状态
  • childView.setPressed(true); 设置其press为true
  • onItemChildClick(baseQuickAdapter, childView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());调用onItemChildClick回调事件。
  • resetPressedView(childView); 重置childview的状态
  • return true; 表示该事件已被消费

如果不出于点击范围内或者isEable=false 则 childView.setPressed(false);

上面处理了item下的childview 的点击,如果是item维度的点击,则执行下面代码,流程基本一致,只是回调的是onItemClick方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
                setPressViewHotSpot(e,pressedView);
                mPressedView.setPressed(true);
                for (Integer childClickViewId : childClickViewIds) {
                    View childView = pressedView.findViewById(childClickViewId);
                    if (childView!=null){
                        childView.setPressed(false);
                    }
                }
                onItemClick(baseQuickAdapter, pressedView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
            } 

之前处理的是存在childview 的情况,如果不存在childview,自然点击事件都是item的了,操作跟在存在childview但childview不处于点击范围内或者isEable=false时类似。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
else {
                setPressViewHotSpot(e,pressedView);
                mPressedView.setPressed(true);
                if (childClickViewIds != null && childClickViewIds.size() > 0) {
                    for (Integer childClickViewId : childClickViewIds) {
                        View childView = pressedView.findViewById(childClickViewId);
                        if (childView!=null){
                            childView.setPressed(false);
                        }
                    }
                }

                onItemClick(baseQuickAdapter, pressedView, vh.getLayoutPosition() - baseQuickAdapter.getHeaderLayoutCount());
            }
            resetPressedView(pressedView);长点击的实现类似,这里就不做重复的分析了。如果大家有哪里不懂的可以直接留言

总结:通过GestureDetectorCompat的使用,能很好的简化我们的编码量,在涉及到交互的操作时,都可以使用GestureDetectorCompat来进行辅助。最好的老师就是阅读优秀的源码,欢迎一起学习,共同进步!