RecyclerView 事件分发原理实战分析,历经 30 天
}
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mIgnoreMotionEventTillDown) {
mIgnoreMotionEventTillDown = false;
}
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
// 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);
break;
case MotionEventCompat.ACTION_POINTER_DOWN:
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);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
} break;
case MotionEventCompat.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.clear();
stopNestedScroll();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
}
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
[](
)分析
mLayoutFrozen
用于标识 RecyclerView 是否禁用了 layout 过程和 scroll 能力,RecyclerView 提供了对其设置的方法setLayoutFrozen(boolean frozen)
, 如果 mLayoutFrozen 被标识为 true, RecyclreView 会发生如下变化:
所有对 RecyclerView 的 Layout 请求会被推迟执行,直到 mLayoutFrozen 再度被设置 false
子 View 也不会被刷新
RecyclerView 也不会响应滑动的请求,即不会响应
smoothScrollBy(int, int)
,scrollBy(int, int)
,scrollToPosition(int)
,smoothScrollToPosition(int)
不响应 Touch Event 和 GenericMotionEvents
如果 RecyclerView 设置了
OnItemTouchListener
, 则在 RecyclerView 自身滑动前,调用dispatchOnItemTouchIntercept(MotionEvent e)
进行分发,代码如下:
private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
mActiveOnItemTouchListener = null;
}
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
mActiveOnItemTouchListener = listener;
return true;
}
}
return false;
}
a. mActiveOnItemTouchListener
是 OnItemTouchListener
类型的对象,如果收到了 ACTION_CANCEL 或者 ACTION_DOWN 事件,则将回调置 null, 清除上个事件序列对本次事件序列的影响,那我们什么时候会收到 ACTION_CANCEL
事件呢?答案是当子 View 正在消费 ACTION_MOVE
事件时,如果父 View 在 onInterceptTouchEvent()
中 return true, 那么子 View 会收到 ACTION_CANCEL
事件,而且这个 ACTION_CANCEL
事件无法被父 View 拦截。
b. 遍历所有注册过的 OnItemTouchListener,如果当前事件不是 ACTION_CANCEL
,调用 OnItemTouchListener
的 onInterceptTouchEvent()
, 并 return true, 表示 RecyclerView 拦截了这个 事件序列,根据事件分发规则,事件被分发到 RecyclerView 的 onTouchEvent()
中,如果满足滑动条件,RecyclerView 会对其进行消费,使自身滑动。
[](
)添加 OnItemTouchListener
为什么不能解决问题?
通过以上线索,我们得到了答案,为什么在 OnItemTouchListener
的方案会失败会失败,
如果
listener.onInterceptTouchEvent(this, e)
return true, 则 RecyclerView 的onInterceptTouchEvent()
会 return true, 事件转向了 RecyclerView 的onTouchEvent()
被消费。如果
listener.onInterceptTouchEvent(this, e)
return false, 则 RecyclerView 还是继续会对这组 MOVE 事件做处理,最终事件转向了 RecyclerView 的onTouchEvent()
被消费。
[](
)最终解决方案
最终结局方案其实和使用 OnItemTouchListener
的 onInterceptTouchEvent
一致,不同的是,这次我们新建一个 RecyclerView 的子类,重写 RecyclerView 的 onInterceptTouchEvent
,具体代码如下:
/**
自定义 RecyclerView ,在某些场景下拦截其横向水平移动
Designed by 0xCAFEBOY
*/
public class InterceptHScrollRecyclerView extends Recycler
View {
private final String TAG = InterceptHScrollRecyclerView.class.getSimpleName();
/**
纵坐标偏移量阈值,超过这个
*/
private final int Y_AXIS_MOVE_THRESHOLD = 15;
public InterceptHScrollRecyclerView(Context context) {
super(context);
}
public InterceptHScrollRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public InterceptHScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
int downY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
downY = (int) e.getRawY();
} else if (e.getAction() == MotionEvent.ACTION_MOVE) {
int realtimeY = (int) e.getRawY();
int dy = Math.abs(downY - realtimeY);
if (dy > Y_AXIS_MOVE_THRESHOLD) {
return false;
}
}
return super.onInterceptTouchEvent(e);
}
}
为什么这个方案可以解决问题,是因为如果使用继承的话,这段代码相当于在 RecyclerView 执行事件分发流程之前插入了一段代码,有点 AOP 的感觉,如果 return false, 可以彻底避免 RecyclerView 接管事件,从而实现目的,注意最后这行代码,
return super.onInterceptTouchEvent(e);
不能直接返回 true, 因为如果不拦截的话,具体的返回值还是 RecyclerView 内部抉择。
[](
)最后
评论