写点什么

RecyclerView 事件分发原理实战分析,历经 30 天

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

}


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;


}

[](

)分析


  1. 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


  1. 如果 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. mActiveOnItemTouchListenerOnItemTouchListener 类型的对象,如果收到了 ACTION_CANCEL 或者 ACTION_DOWN 事件,则将回调置 null, 清除上个事件序列对本次事件序列的影响,那我们什么时候会收到 ACTION_CANCEL 事件呢?答案是当子 View 正在消费 ACTION_MOVE 事件时,如果父 View 在 onInterceptTouchEvent() 中 return true, 那么子 View 会收到 ACTION_CANCEL 事件,而且这个 ACTION_CANCEL 事件无法被父 View 拦截。


b. 遍历所有注册过的 OnItemTouchListener,如果当前事件不是 ACTION_CANCEL ,调用 OnItemTouchListeneronInterceptTouchEvent() , 并 return true, 表示 RecyclerView 拦截了这个 事件序列,根据事件分发规则,事件被分发到 RecyclerView 的 onTouchEvent() 中,如果满足滑动条件,RecyclerView 会对其进行消费,使自身滑动。


[](


)添加 OnItemTouchListener 为什么不能解决问题?




通过以上线索,我们得到了答案,为什么在 OnItemTouchListener 的方案会失败会失败,


  1. 如果 listener.onInterceptTouchEvent(this, e) return true, 则 RecyclerView 的 onInterceptTouchEvent() 会 return true, 事件转向了 RecyclerView 的 onTouchEvent() 被消费。

  2. 如果 listener.onInterceptTouchEvent(this, e) return false, 则 RecyclerView 还是继续会对这组 MOVE 事件做处理,最终事件转向了 RecyclerView 的 onTouchEvent() 被消费。


[](


)最终解决方案




最终结局方案其实和使用 OnItemTouchListeneronInterceptTouchEvent 一致,不同的是,这次我们新建一个 RecyclerView 的子类,重写 RecyclerView 的 onInterceptTouchEvent,具体代码如下:


/**


  • 自定义 RecyclerView ,在某些场景下拦截其横向水平移动

  • Designed by 0xCAFEBOY


*/


public class InterceptHScrollRecyclerView extends Recycler


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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 内部抉择。


[](


)最后

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
RecyclerView 事件分发原理实战分析,历经30天