【Android】RecyclerView滑动事件处理与源代码解析

问题

当前有一个需求,就是在RV中需要完成一些item的侧滑功能,但是中间遇到了一些很有意思的情况,所以这里记录一下,首先有个很奇怪的情况,就是当我们在RV的item中存在滑动事件的时候,不管怎么样都会处理到ACTION_DOWN事件(在RV intercept方法中不添加不拦截也是这样)。

追到RV的onInterceptTouchEvent方法去,首先注释就说明了当发生ACTION_DOWN的时候,也就是点击的时候,RV是不会拦截这个事件的,会主动的把这个DOWN事件传递下去

...
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;
}
...

那我又有一个疑问了,为什么我明明在ACTION_MOVE中对当前事件进行了一个判断,判断当前事件是否进行拦截,如果进行了拦截为什么这个事件还会传递到自控件呢?首先我们要明白一点,就是说咱们的事件一定是按照流程进行的:例如ACTION_DOWN->ACTION_MOVE->ACTION_UP。如果在ACTION_MOVE中进行拦截,确实是拦截到了,后续的MOVE不会传递过去了,但是之前的ACTION_DOWN事件已经被传递到子控件里面去了,既然已经传递过去了,那么只要子控件里面的onTouchEvent()返回了true,说明这个事件已经被消耗掉了。所以说在RV中的ACTION_MOVE中进行事件拦截判断感觉不太靠谱。

那么到底哪里可以真正的拦截一个事件呢?当然是从他的源头开始ACTION_DOWN!假如我们在那里直接返回一个true,那么后续的事件就可以和子控件彻底说拜拜了!因为只要RV拦截了当前的事件,就会到他的touchEvent方法里面去处理这个事件。

MotionEvent.ACTION_DOWN -> {
    Log.d("MainActivity", "RV down")
    return true
}

那么我们到底要怎么做才可以在RV中完成我们的需求呢?

首先有两种解决方案,一种是在RV中可以获取到当前点击的item是哪一个,然后再RV中直接对这个item进行操作,但是这样的方式灵活性比较低,所以我会着重介绍第二种方式,就是主要在item里面对事件进行处理,接下来我会介绍一下如何处理事件,为了能够对布局的onTouchEvent方法进行处理,首先我们需要先引入布局(相当于现在是一个自定义view了)。

class LeftSlideItem(context: Context, attributeSet: AttributeSet?): LinearLayout(context, attributeSet) {
    val mView:View = LayoutInflater.from(context).inflate(R.layout.left_slide_item, this, true)
    val content = mView.findViewById<LinearLayout>(R.id.content)
    val delete = mView.findViewById<TextView>(R.id.deleteText)
    var dx = 0f
    var dy = 0f
    var curX = 0f
    var curY = 0f
    var preX = 0f
    var preY = 0f
  ...
}

处理事件的重点在于onTouchEvent方法里,我们在RV中不需要使用requestDisallowInterceptTouchEvent方法,因为RV已经帮我们处理好了。我们只需要在

onTouchEvent方法里根据当前的情况返回true(true表示就是消费了这个事件!只有当我们消费了ACTION_DOWN这个事件,后续的事件才会跟着进来),下面代码的意思就是当横向滑动距离高于竖直的时候,就开始处理item中的滑动,不然就把这个事件重新交给父容器(也就是RecyclerView)处理。

override fun onTouchEvent(event: MotionEvent): Boolean {
  ...
  if(abs(curX - preX) > abs(curY - preY)){
    preX = curX
    preY = curY
    Log.d("MainActivity", "子控件消耗事件")
    return true     //只有返回true后续才会有事件进来?
  }
  preX = curX
  preY = curY
	return super.onTouchEvent(event)
}

到这里整个流程就结束了,以上就是完成需求中事件处理的一个整体思路。


Watch the fucking source code

接下来,我会介绍一下RV中拦截滑动事件的源代码,先来看看处理action的上面一部分。

public boolean onInterceptTouchEvent(MotionEvent e) {
  if (mLayoutSuppressed) {
    //1
    // 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;
  }

  // Clear the active onInterceptTouchListener.  None should be set at this time, and if one
  // is, it's because some other code didn't follow the standard contract.
  mInterceptingOnItemTouchListener = null;
  if (findInterceptingOnItemTouchListener(e)) {
    //2
    cancelScroll();
    return true;
  }

  if (mLayout == null) {
    return false;	//3
  }

  //4
  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();
	
  ...
	
  return mScrollState == SCROLL_STATE_DRAGGING;
}

注释1那边中提到了如果布局被点击了,他首先不会处理这个这个事件直接让事件被子控件处理。

然后在注释2那个地方就是我们可以为item添加一些点击事件(会调用addOnItemTouchListener方法),如果说添加了ItemTouchListener就会看看ItemTouchLishtener会不会根据当前的事件进行拦截,如果拦截了,那么RV就拦截当前事件!拦截事件以后在onTouchEvent里面会执行ItemTouchLishtener对应的onTouchEvent,如果这里item可以把事件消耗掉,那么事件到这里就结束了,所以总的来说注释2那边就是看看是否添加了ItemTouchListener,如果有就执行相应的操作。

private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
  int action = e.getAction();
  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) {
      mInterceptingOnItemTouchListener = listener;
      return true;
    }
  }
  return false;
}

再到后面注释3那里的mLayout就是LayoutManager,如果我们没有设置这个成员变量的,RV是不会拦截事件的。再往下走,到注释4那边基本上就是一些为后续判断当前动作所做的一些准备了,现在我们来到判断事件动作的代码里面,然后分事件来解释。

首先看一看事件的起点,第一行代码getPointerId是为了获取第一个触摸点(有些设备支持多点触碰,官方文档https://developer.android.com/reference/android/view/MotionEvent)。后面提到了mScrollState的状态,总共是有三种状态。然后在下面判断当前是否为自动滚动,如果是就要停止滚动,并且把当前的状态设置为手指滚动,开始滑动。

//停止滚动
public static final int SCROLL_STATE_IDLE = 0;
 
//正在被外部拖拽,一般为用户正在用手指滚动
public static final int SCROLL_STATE_DRAGGING = 1;
 
//自动滚动开始
public static final int SCROLL_STATE_SETTLING = 2;
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);
    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;

然后看看ACTION_MOVE代码,在不断计算当前的x轴和y轴与之前的变化,并且设置了滚动的状态

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 = x;
      startScroll = true;
    }
    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
      mLastTouchY = y;
      startScroll = true;
    }
    if (startScroll) {
      setScrollState(SCROLL_STATE_DRAGGING);
    }
  }
} break;

然后就到了ACTION_UP,也很简单,就是清除了一些标记。

case MotionEvent.ACTION_UP: {
    mVelocityTracker.clear();
    stopNestedScroll(TYPE_TOUCH);
} break;

总的来说在onIntercept方法里主要就干了两件事,第一件事就是当前这个事件是不是要RV进行处理,第二件事就是如果是自己去处理这个事件的话,设置了一些与滑动相关的变量。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值