Android中的事件冲突源码大解密

Android作为一门前端技术,界面与交互占了很大的比重,既然有交互,就一定有点击和滑动事件,有事件就一定会有冲突。因为屏幕上不可能只有一个单一的控件,一定是多个控件层叠在一起。很多小伙伴对滑动冲突不太了解,笔者在做Android的前几年也一直搞不清楚,最近狠下心来决定好好的研究一下源码,从根本上解决Android滑动冲突的问题,在学习的过程中,写下这篇博客,供更多的小伙伴参考。

本文目录:

1654772971(1).png

写在前面:

本文大篇幅都是在分析源码,可能会比较枯燥,建议稍稍有些Android基础的小伙伴观看。

  1. 什么是事件冲突呢?

当事件只有一个,但是有多个对象想要处理,处理事件的那个对象并不是我们想要的那个对象。这个时候我们认为就是发生了事件冲突。

  1. 在我们手指触碰手机屏幕的过程中,会有哪些事件呢?

单点触碰时,当我们用手指在手机屏幕上按下的时候,就会出现一个ACTION_DOWN事件。 当我们手指在屏幕上滑动的时候,会有一系列的ACTION_MOVE事件。当我们手指抬起的时候,出现一个ACTION_UP事件。另外还有一个ACTION_CANCEL事件,当事件被上层控件拦截时触发。

那么问题来了,这些事件都是怎么传递的? 本文就是通过看源码来分析ACTION_DOWN事件和ACTION_MOVE事件的传递和处理过程,从而从根本上解决滑动冲突这个疑难问题。

在后面看源码的时候,我们可以知道的是,只有ACTION_DOWN事件才会被分发,而ACTION_MOVE事件是不会被分发的。先记住这个结论。 另外还需要记住的结论是:只有继承自ViewGroup的自定义View会去分发事件,继承自View的自定义View只能处理事件。

一、ACTION_DOWN事件是如何传递的

如下图所示:

1654686761(1).png

当手机点击屏幕时,首先响应的是Activity的dispatchTouchEvent(),下图从代码的角度分析了这一流程:

1654687464(1).png

 // Activity的 dispatchTouchEvent
 public boolean dispatchTouchEvent(MotionEvent ev) {
	   if (ev.getAction() == MotionEvent.ACTION_DOWN) {
		   onUserInteraction();
	   }
	   // 这里调用了getWindow()的superDispatchTouchEvent(ev)
	   if (getWindow().superDispatchTouchEvent(ev)) {
		   return true;
	   }
	   return onTouchEvent(ev);
   }

 ====>  getWindow: 
    // 其中的window,是Window类的实现,Android中Window类的实现只有一个 : PhoneWindow 
    private Window mWindow;
    public Window getWindow() {
        return mWindow;
    }

====> PhoneWindow:
// 来看PhoneWindow中,superDispatchTouchEvent 
   private DecorView mDecor;
   @Override
   public boolean superDispatchTouchEvent(MotionEvent event) {
       // 这里的mDecor是一个DecorView
	   return mDecor.superDispatchTouchEvent(event);
   }


====> DecorView:
	   public boolean superDispatchTouchEvent(MotionEvent event) {
	           // 这里调用的是父类的dispatchTouchEvent DecorView继承FrameLayout ,
	           // 因此,最终调用的是ViewGroup的dispatchTouchEvent
			   return super.dispatchTouchEvent(event);
		   }

复制代码

看懂了上面一系列的分析,就可以知道,事件的分发,真正走的是ViewGroup的dispatchTouchEvent方法。 那么我们下面就来到ViewGroup的dispatchTouchEvent方法中:
关键代码:根据intercepted来判断是否要拦截事件。

1654690756(1).png

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                // 如果是ACTION_DOWN事件,先把之前的这些状态清零
                cancelAndClearTouchTargets(ev);
                resetTouchState(); // 清除了mGroupFlags的值 disallowIntercept为false
            }

            // Check for interception.
            // 在这里判断 事件是否要拦截
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // 第一次进来  disallowIntercept = false
				if (!disallowIntercept) {
					// 这里肯定会走
					// 是否拦截 根据onInterceptTouchEvent的返回值决定
					// true 表示拦截      false表示不拦截 
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
}

/**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

复制代码

由上面的代码可以看出,ViewGroup中down事件是否要拦截,还是要根据onInterceptTouchEvent的返回值决定。 那么onInterceptTouchEvent返回的true或false,接下来的代码流程有什么不同呢?

1. 返回true的情况

如果返回true,说明ViewGroup要拦截这个事件,拦截事件的代码流程如下图:

1654691603(1).png

        @Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
			...
			// 此时mFirstTouchTarget还未赋过值,为null
			 if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 肯定会走这里
                // dispatchTransformedTouchEvent 是否处理事件
                // 第三个参数为空,这个参数是childView
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }
			 ...
	 }

	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
            if (child == null) {

	    // 这里调用的是View的dispatchTouchEvent
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            // 调用child的dispatchTouchEvent 看child是View还是ViewGroup 
            // 如果是View 就走View的dispatchTouchEvent  
            // 如果是ViewGroup 就走ViewGroup的dispatchTouchEvent
            handled = child.dispatchTouchEvent(transformedEvent);
        }
		...
				}
复制代码

拦截事件,意思就是你是最后一个,必须选择事件是否处理。第一次进入dispatchTransformedTouchEvent时,会去调用View的dispatchTouchEvent。

View的dispatchTouchEvent会去处理事件,前面说过,继承自View的自定义View,只负责处理事件,不分发事件。

2. 返回false的情况

如果返回false,说明ViewGroup不打算拦截事件,也就是要把事件往下分发。那么intercepted=false,代码在dispatchTouchEvent中会进入到下面的代码中:

1654757644(1).png

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
             ...
            // 不拦截事件 走这里
            if (!canceled && !intercepted) {         
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                // 只有ACTION_DOWN事件,才会进行事件的分发
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...
                        final View[] children = mChildren;
						// 倒叙取出 ViewGroup会轮询每个子View 是否要处理这个事件
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
							// 取出这个ViewGroup中的各个child
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
							// child是否可见(不可见要判断View是否设置了动画)
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }
                           ...
                            resetCancelNextUpFlag(child);
							// 是否分发事件 第三个参数传入 child 在这里把事件传递给子View
							// 会调用   handled = child.dispatchTouchEvent(transformedEvent);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
								// 如果为true 说明child要处理这个事件 
                                ...
                              // 所以这句代码执行完后, target.next == null ; mFirstTouchTarget!=null;
                              //                                newTouchTarget = mFirstTouchTarget = target;                              
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;// 跳出循环,有一个子View处理了,就不再向下轮询了
                            }                           
                   ...
		}
     private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
		View child, int desiredPointerIdBits) {
				...
				if (child == null) {
					// 这里调用的是View的dispatchTouchEvent
					handled = super.dispatchTouchEvent(transformedEvent);
				} else {
					...
					// 调用child的dispatchTouchEvent 看child是View还是ViewGroup 在这里把事件分发给了子View
					handled = child.dispatchTouchEvent(transformedEvent);
				}
		...
	}

复制代码

此时得到的结论是:
1.如果不拦截事件,最终调用的是child.dispatchTouchEvent(transformedEvent);也就是把事件分发下去了,此时要看child是View还是ViewGroup。
2. 只有ACTION_DOWN事件才会进行事件的分发。
3. 得到了下面几个值:

target.next == null ; 
mFirstTouchTarget!=null;
newTouchTarget = mFirstTouchTarget = target;        
alreadyDispatchedToNewTouchTarget = true;
复制代码

此时代码会继续往下走:
(这里要注意一下,for循环轮询子View要不要处理这个事件,如果所有的子View都不处理的话,这几个值都不会改变,那么代码继续往下走,走的就是拦截事件的流程了。(分发给你你不要,和直接拦截不分发给你是一样的。)也就是说,此时事件ViewGroup要拦截掉了,事件回到了上层的ViewGroup手中了。)

image.png

代码走到这里就结束了,父View不处理该事件,子View已经处理过了。

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
      // 子View已经处理了 父View就不处理了
       handled = true;
  } 
复制代码

二、ACTION_MOVE事件是如何传递的

ACTION_DOWN事件处理完成后,我们就可以知道,是哪个View处理事件了。ACTION_DOWN事件结束后,ACTION_MOVE事件来了,ACTION_MOVE事件虽然不能分发,但依然要从最上层往下传递,走dispatchTouchEvent方法。最上层的ViewGroup直接找到要处理这个事件的View,直接把事件给它。

MOVE事件仍然是从ViewGroup的dispatchTouchEvent出发,依次向下调用,由于参数的值不同,所以代码流程和ACTION_DOWN事件的代码流程有点区别。如下图所示:

1654764941(1).png

如图中所示: 走到最后,dispatchTransformedTouchEvent()时,会调用child.dispatchTouchEvent(transformedEvent); 此时如果child是ViewGroup,会以同样的流程再走一遍代码,直到child为View,调用View的dispatchTouchEvent处理事件。

三、解决滑动冲突的小实战

前面铺垫了那么多,这里终于来到了实战。处理事件冲突,这里的案例是一个ViewPager包裹了一个ListView,ListView负责上下滑动,ViewPager负责左右滑动。

处理事件冲突往往有两种方法:

  1. 内部拦截法 (子View处理事件)
  2. 外部拦截法 (父容器处理事件)

1. 内部拦截法

内部拦截法,就是重写子View的dispatchTouchEvent方法,在其中添加特殊处理的代码:

      // 内部拦截法:子view处理事件冲突
    private int mLastX, mLastY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
               // 按下时,事件交给自己
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                // 左右滑动时,事件交给父控件 
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;

            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }
复制代码

requestDisallowInterceptTouchEvent()是ViewGroup中改变disallowIntercept值的方法:代码如下

1654771832(1).png

如果传入true:

mGroupFlags |= FLAG_DISALLOW_INTERCEPT & FLAG_DISALLOW_INTERCEPT != 0  为true
复制代码

如果传入false

mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT  & FLAG_DISALLOW_INTERCEPT != 0 为false
复制代码

这个方法是通过改变mGroupFlags的值,改变了disallowIntercept的值。
当disallowIntercept = true 时,查看源码可以知道,intercepted = false;也就是父容器不会去拦截事件,事件交给子View处理。
当disallowIntercept = false时,intercepted = onInterceptTouchEvent(ev),是否拦截由onInterceptTouchEvent(ev)的返回值决定。

具体源码中对这里的描述在ViewGroup的dispatchTouchEvent方法中:

1654770885(1).png

但是此时,仍然无法左右滑动,这是为什么呢?查看源码发现,源码中在ViewGroup的dispatchTouchEvent方法中,判断是否拦截事件的之前有一段代码如下所示:

1654771224(1).png

红框中的代码,ACTION_DOWN事件时,清除了mGroupFlags的值,使disallowIntercept=false,如下图所示: 1654771675(1).png

因此是否拦截事件由父容器的onInterceptTouchEvent(ev)的返回值决定,所以需要在父容器中重写onInterceptTouchEvent方法:在ACTION_DOWN事件时,手动返回false,不拦截此事件,交给子View去处理。

public boolean onInterceptTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN){
            // 在ACTION_DOWN事件时
            super.onInterceptTouchEvent(event);
            return false;
        }
       return true;
}
复制代码

再次运行会发现,我们想要的效果已经达到了。

2. 外部拦截法

外部拦截法,就是父容器处理事件,根据重写父容器的onInterceptTouchEvent来手动的控制父容器是否要拦截当前的事件。

   private int mLastX, mLastY;
   @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = (int) event.getX();
                mLastY = (int) event.getY();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                // 左右滑动时,我要拦截事件
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }
        // 其他的情况,直接调用super.onInterceptTouchEvent(event); 安卓源码中已经做过处理
        return super.onInterceptTouchEvent(event);

    }
复制代码

到这里,Android中事件冲突的问题,在源码中已经分析清楚了,最近在重新学习自定义View,后续会更新一系列的文章,会根据自己的理解,把所学到的东西分享出来,供更多的小伙伴参考。

猜你喜欢

转载自juejin.im/post/7106844858765017124
今日推荐