问题
当前有一个需求,就是在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进行处理,第二件事就是如果是自己去处理这个事件的话,设置了一些与滑动相关的变量。