文章目录
概述
RecyclerView作为一个灵活的在有限窗口显示大量数据集的视图组件,继承自ViewGroup,需要处理触摸事件产生时子View的滚动。同时RecyclerView实现了NestedScrollingChild接口,也支持嵌套在支持Nested的父容器中。
这里结合LinearLayoutManager,以垂直方向滑动为例,从源码浅析RecyclerView是如何进行滑动事件处理的。
源码探究
文中源码基于 ‘androidx.recyclerview:recyclerview:1.1.0’
RecyclerView中的处理
RecyclerView和常规事件处理方式一样,重写了onInterceptTouchEvent和onTouchEvent。RecyclerView也实现了NestedScrollingChild接口,在关键事件节点也会通知实现了NestedScrollingParent接口的父容器。
关于NestedScrollingChild和NestedScrollingParent的简要用法和说明,可参考《关于NestedScrollingParent2、NestedScrollingChild2接口》
onInterceptTouchEvent
[RecyclerView#onInterceptTouchEvent]
public boolean onInterceptTouchEvent(MotionEvent e) {
// 判断是否抑制布局滚动,可通过suppressLayout方法设置为true,当重新设置Adapter或托管item动画时不拦截。
if (mLayoutSuppressed) {
// When layout is suppressed, RV does not intercept the motion event.
// A child view e.g. a button may still get the click.
return false;
}
// 省略OnItemTouchListener部分,设置FastScroller或ItemTouchHelper时涉及 ···
if (mLayout == null) {
return false;
}
// 获取支持滚动的方向。以垂直排列的LinearLayoutManager为例,canScrollVertically为true。
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
// mIgnoreMotionEventTillDown默认为false,调用suppressLayout抑制布局滚动时会将其置为true
if (mIgnoreMotionEventTillDown) {
mIgnoreMotionEventTillDown = false;
}
// 获取第一个触摸点的ID
mScrollPointerId = e.getPointerId(0);
// 保存DOWN时X、Y坐标,用于计算滑动偏移量
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
// 判断当前滑动状态是否是惯性滑动或其他非用户触摸滑动,mScrollState默认为SCROLL_STATE_IDLE
if (mScrollState == SCROLL_STATE_SETTLING) {
// 请求父布局不拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
// 更新滑动状态为SCROLL_STATE_DRAGGING
setScrollState(SCROLL_STATE_DRAGGING);
// 通知父布局停止滑动,类型为TYPE_NON_TOUCH
stopNestedScroll(TYPE_NON_TOUCH);
}
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
// 获取当前支持的滑动方向
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
// 通知父布局滑动即将开始
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_DOWN:
// 有新的触摸点,更新触摸点ID和初始X、Y坐标以新的为准
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
// 获取最新触摸点的当前位置
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
// 判断当前滑动状态是否是SCROLL_STATE_DRAGGING
if (mScrollState != SCROLL_STATE_DRAGGING) {
// 计算滑动偏移量
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
// 标记是否有任一方向可以滑动
boolean startScroll = false;
// 判断是否构成滑动,mTouchSlop为最小滑动距离
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
// 保存刚开始滑动时的坐标
mLastTouchX = x;
startScroll =