作为一名 Android 开发者, 每天接触最多的就是 View 了. Android View 虽然不是四大组件, 但其并不比四大组件的地位低. 而 View 的核心知识点事件分发机制则是不少刚入门同学的拦路虎, 也是面试过程中基本上都会问的. 理解 View 的事件能够让你写出更好自定义 View 以及解决滑动冲突.
1, View 事件认识
1.1 MotionEvent 事件
当你用手指轻触屏幕, 这个过程在 Android 中主要可以分为以下三个过程:
ACTION_DOWN: 手指刚接触屏幕, 按下去的那一瞬间产生该事件
ACTION_MOVE: 手指在屏幕上移动时候产生该事件
ACTION_UP: 手指从屏幕上松开的瞬间产生该事件
从 ACTION_DOWN 开始到 ACTION_UP 结束我们称为一个事件序列
正常情况下, 无论你手指在屏幕上有多么骚的操作, 最终呈现在 MotionEvent 上来讲无外乎下面两种动作.
点击(点击后抬起, 也就是单击操作):ACTION_DOWN -> ACTION_UP
滑动(点击后再滑动一段距离, 再抬起):ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
1.2 理论知识
public boolean dispatchTouchEvent(MotionEvent ev)
return true: 表示消耗了当前事件, 有可能是当前 View 的 onTouchEvent 或者是子 View 的 dispatchTouchEvent 消费了, 事件终止, 不再传递.
return false: 调用父 ViewGroup 或 Activity 的 onTouchEvent. (不再往下传).
return super.dispatherTouchEvent: 则继续往下 (子 View ) 传递, 或者是调用当前 View 的 onTouchEvent 方法;
总结: 用来分发事件, 即事件序列的大门, 如果事件传递到当前 View 的 onTouchEvent 或者是子 View 的 dispatchTouchEvent, 即该方法被调用了. 另外如果不消耗 ACTION_DOWN 事件, 那么 down, move, up 事件都与该 View 无关, 交由父类处理(父类的 onTouchEvent 方法)
public boolean onInterceptTouchEvent(MotionEvent ev)
return true:ViewGroup 将该事件拦截, 交给自己的 onTouchEvent 处理.
return false: 继续传递给子元素的 dispatchTouchEvent 处理.
return super.dispatherTouchEvent: 事件默认不会被拦截.
总结: 在 dispatchTouchEvent 内部调用, 顾名思义就是判断是否拦截某个事件.(注: ViewGroup 才有的方法, View 因为没有子 View 了, 所以不需要也没有该方法) . 而且这一个事件序列 (当前和其它事件) 都只能由该 ViewGroup 处理, 并且不会再调用该 onInterceptTouchEvent 方法去询问是否拦截.
public boolean onTouchEvent(MotionEvent ev)
return true: 事件消费, 当前事件终止.
return false: 交给父 View 的 onTouchEvent.
return super.dispatherTouchEvent: 默认处理事件的逻辑和返回 false 时相同.
总结: 在 dispatchTouchEvent 内部调用
上面三个方法之间的调用关系可以用下面的代码表示:
- public boolean dispatchTouchEvent(MotionEvent ev) {
- boolean consume = false;// 事件是否被消费
- if (onInterceptTouchEvent(ev)){// 调用 onInterceptTouchEvent 判断是否拦截事件
- consume = onTouchEvent(ev);// 如果拦截则调用自身的 onTouchEvent 方法
- }else{
- consume = child.dispatchTouchEvent(ev);// 不拦截调用子 View 的 dispatchTouchEvent 方法
- }
- return consume;// 返回值表示事件是否被消费, true 事件终止, false 调用父 View 的 onTouchEvent 方法
- }
1.3 事件传递顺序
对于一个点击事件, Activity 会先收到事件的通知, 接着再将其传给 DecorView(根 view), 通过 DecorView 在将事件逐级进行传递. 具体传递逻辑见下图:
可以看出事件的传递过程都是从父 View 到子 View. 但是这里有三点需要特别强调一下
子 View 可以通过 requestDisallowInterceptTouchEvent 方法干预父 View 的事件分发过程( ACTION_DOWN 事件除外), 而这就是我们处理滑动冲突常用的关键方法.
对于 View(注意! ViewGroup 也是 View)而言, 如果设置了 onTouchListener, 那么 OnTouchListener 方法中的 onTouch 方法会被回调. onTouch 方法返回 true, 则 onTouchEvent 方法不会被调用 (onClick 事件是在 onTouchEvent 中调用) 所以三者优先级是 onTouch->onTouchEvent->onClick
View 的 onTouchEvent 方法默认都会消费掉事件(返回 true), 除非它是不可点击的(clickable 和 longClickable 同时为 false),View 的 longClickable 默认为 false,clickable 需要区分情况, 如 Button 的 clickable 默认为 true, 而 TextView 的 clickable 默认为 false.
2,View 事件分发源码
先从 Activity 中的 dispatchTouchEvent 方法出发:
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- return super.dispatchTouchEvent(ev);
- }
Activity 将事件传给父 Activity 来处理, 下面看父 Activity 是怎么处理的.
- /**
- * Called to process touch screen events. You can override this to
- * intercept all touch screen events before they are dispatched to the
- * window. Be sure to call this implementation for touch screen events
- * that should be handled normally.
- *
- * @param ev The touch screen event.
- *
- * @return boolean Return true if this event was consumed.
- */
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- if (getWindow().superDispatchTouchEvent(ev)) {
- return true;
- }
- return onTouchEvent(ev);
- }
其中有个 onUserInteraction 方法, 该方法是只要用户在 Activity 的任何一处点击或者滑动都会响应, 一般不使用. 接下去看 getWindow().superDispatchTouchEvent(ev) 所代表的具体含义. getWindow() 返回对应的 Activity 的 window. 一个 Activity 对应一个 Window 也就是 PhoneWindow, 一个 PhoneWindow 持有一个 DecorView 的实例, DecorView 本身是一个 FrameLayout. 这句话一定要牢记.
- /**
- * Retrieve the current {@link android.view.Window} for the activity.
- * This can be used to directly access parts of the Window API that
- * are not available through Activity/Screen.
- *
- * @return Window The current window, or null if the activity is not
- * visual.
- */
- public Window getWindow() {
- return mWindow;
- }
Window 的源码有说明 The only existing implementation of this abstract class is
android.view.PhoneWindow,Window 的唯一实现类是 PhoneWindow. 那么去看 PhoneWindow 对应的代码.
- public boolean superDispatchTouchEvent(MotionEvent event) {
- return mDecor.superDispatchTouchEvent(event);
- }
PhoneWindow 又调用了 DecorView 的 superDispatchTouchEvent 方法. 而这个 DecorView 就是 Window 的根 View, 我们通过 setContentView 设置的 View 是它的子 View(Activity 的 setContentView, 最终是调用 PhoneWindow 的 setContentView )
到这里事件已经被传递到根 View 中, 而根 View 其实也是 ViewGroup. 那么事件在 ViewGroup 中又是如何传递的呢?
2.1 ViewGroup 事件分发
- public boolean dispatchTouchEvent(MotionEvent ev) {
- ......
- final int action = ev.getAction();
- final int actionMasked = action & MotionEvent.ACTION_MASK;
- if (actionMasked == MotionEvent.ACTION_DOWN) {
- cancelAndClearTouchTargets(ev);
- // 清除 FLAG_DISALLOW_INTERCEPT, 并且设置 mFirstTouchTarget 为 null
- resetTouchState(){
- if(mFirstTouchTarget!=null){mFirstTouchTarget==null;}
- mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- ......
- };
- }
- final boolean intercepted;//ViewGroup 是否拦截事件
- // mFirstTouchTarget 是 ViewGroup 中处理事件 (return true) 的子 View
- // 如果没有子 View 处理则 mFirstTouchTarget=null,ViewGroup 自己处理
- if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
- final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
- if (!disallowIntercept) {
- intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent
- ev.setAction(action);
- } else {
- intercepted = false;
- // 如果子类设置 requestDisallowInterceptTouchEvent(true)
- //ViewGroup 将无法拦截 MotionEvent.ACTION_DOWN 以外的事件
- }
- } else {
- intercepted = true;
- //actionMasked != MotionEvent.ACTION_DOWN 并且没有子 View 处理事件, 则将事件拦截
- // 并且不会再调用 onInterceptTouchEvent 询问是否拦截
- }
- ......
- ......
- }
先看标红的代码, 这句话的意思是: 当 ACTION_DOWN 事件到来时, 或者有子元素处理事件( mFirstTouchTarget != null ), 如果子 view 没有调用 requestDisallowInterceptTouchEvent 来阻止 ViewGroup 的拦截, 那么 ViewGroup 的 onInterceptTouchEvent 就会被调用, 来判断是否是要拦截. 所以, 当子 View 不让父 View 拦截事件的时候, 即使父 View onInterceptTouchEvent 中返回 true 也没用了.
另外, FLAG_DISALLOW_INTERCEPT 这个标记位是通过子 View requestDisallowInterceptTouchEvent 方法设置的. 具体可参看如下代码.
- @Override
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
- // We're already in this state, assume our ancestors are too
- return;
- }
- if (disallowIntercept) {
- mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
- } else {
- mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- }
- // Pass it up to our parent
- if (mParent != null) {
- mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
- }
- }
同时, 如果这个 ViewGroup 有父 View 的时候, 还得让父父 View 不能拦截. 继续看 ViewGroup 的 dispatchTouchEvent 方法.
- public boolean dispatchTouchEvent(MotionEvent ev) {
- final View[] children = mChildren;
- for (int i = childrenCount - 1; i>= 0; i--) {
- final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
- final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
- ......
- if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))
- {
- ev.setTargetAccessibilityFocus(false);
- // 如果子 View 没有播放动画, 而且点击事件的坐标在子 View 的区域内, 继续下面的判断
- continue;
- }
- // 判断是否有子 View 处理了事件
- newTouchTarget = getTouchTarget(child);
- if (newTouchTarget != null) {
- // 如果已经有子 View 处理了事件, 即 mFirstTouchTarget!=null, 终止循环.
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- break;
- }
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
- // 点击 dispatchTransformedTouchEvent 代码发现其执行方法实际为
- //return child.dispatchTouchEvent(event); (因为 child!=null)
- // 所以如果有子 View 处理了事件, 我们就进行下一步: 赋值
- ......
- newTouchTarget = addTouchTarget(child, idBitsToAssign);
- //addTouchTarget 方法里完成了对 mFirstTouchTarget 的赋值
- alreadyDispatchedToNewTouchTarget = true;
- break;
- }
- }
- }
- private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
- final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
- target.next = mFirstTouchTarget;
- mFirstTouchTarget = target;
- return target;
- }
- private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
- View child, int desiredPointerIdBits) {
- ......
- if (child == null) {
- // 如果没有子 View 处理事件, 就自己处理
- handled = super.dispatchTouchEvent(event);
- } else {
- // 有子 View, 调用子 View 的 dispatchTouchEvent 方法
- handled = child.dispatchTouchEvent(event);
- ......
- return handled;
- }
上面为 ViewGroup 对事件的分发, 主要有 2 点
如果有子 View, 则调用子 View 的 dispatchTouchEvent 方法判断是否处理了事件, 如果处理了便赋值 mFirstTouchTarget, 赋值成功则跳出循环.
ViewGroup 的事件分发最终还是调用 View 的 dispatchTouchEvent 方法, 具体如上代码所述.
2.2 View 的事件分发
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
- mOnTouchListener.onTouch(this, event)) {
- return true;
- }
- return onTouchEvent(event);
- }
上述方法只有以下 3 个条件都为真, dispatchTouchEvent() 才返回 true; 否则执行 onTouchEvent().
- mOnTouchListener != null
- (mViewFlags & ENABLED_MASK) == ENABLED
- mOnTouchListener.onTouch(this, event)
这也就说明如果调用了 setOnTouchListener 设置了 listener, 就会先调用 onTouch 方法. 没有的话才会去调用 onTouchEvent 方法. 接下去, 我们看 onTouchEvent 源码.
- public boolean onTouchEvent(MotionEvent event) {
- final int viewFlags = mViewFlags;
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
- return (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
- }
- // 如果进行了事件代理, 就会被拦截, 不会在往下面走了
- if (mTouchDelegate != null) {
- if (mTouchDelegate.onTouchEvent(event)) {
- return true;
- }
- }
- // 若该控件可点击, 则进入 switch 判断中
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
- switch (event.getAction()) {
- // a. 若当前的事件 = 抬起 View(主要分析)
- case MotionEvent.ACTION_UP:
- boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
- ...// 经过种种判断, 此处省略
- // 执行 performClick() ->>分析 1
- performClick();
- break;
- // b. 若当前的事件 = 按下 View
- case MotionEvent.ACTION_DOWN:
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
- mPrivateFlags |= PREPRESSED;
- mHasPerformedLongPress = false;
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- break;
- // c. 若当前的事件 = 结束事件(非人为原因)
- case MotionEvent.ACTION_CANCEL:
- mPrivateFlags &= ~PRESSED;
- refreshDrawableState();
- removeTapCallback();
- break;
- // d. 若当前的事件 = 滑动 View
- case MotionEvent.ACTION_MOVE:
- final int x = (int) event.getX();
- final int y = (int) event.getY();
- int slop = mTouchSlop;
- if ((x <0 - slop) || (x>= getWidth() + slop) ||
- (y <0 - slop) || (y>= getHeight() + slop)) {
- // Outside button
- removeTapCallback();
- if ((mPrivateFlags & PRESSED) != 0) {
- // Remove any future long press/tap checks
- removeLongPressCallback();
- // Need to switch from pressed to not pressed
- mPrivateFlags &= ~PRESSED;
- refreshDrawableState();
- }
- }
- break;
- }
- // 若该控件可点击, 就一定返回 true
- return true;
- }
- // 若该控件不可点击, 就一定返回 false
- return false;
- }
- /**
- * 分析 1:performClick()
- */
- public boolean performClick() {
- if (mOnClickListener != null) {
- playSoundEffect(SoundEffectConstants.CLICK);
- mOnClickListener.onClick(this);
- return true;
- // 只要我们通过 setOnClickListener()为控件 View 注册 1 个点击事件
- // 那么就会给 mOnClickListener 变量赋值(即不为空)
- // 则会往下回调 onClick() & performClick()返回 true
- }
- return false;
- }
从上面的代码我们可以知道, 当手指抬起的时候, 也就是处于 MotionEvent.ACTION_UP 时, 才会去调用 performClick(). 而 performClick 中会调用 onClick 方法.
也就说明了: 三者优先级是 onTouch->onTouchEvent->onClick
至此 View 的事件分发机制讲解完毕.
参考文献:
1,Android View 的事件分发机制和滑动冲突解决
2, 一文读懂 Android View 事件分发机制
3,Android 事件分发机制详解: 史上最全面, 最易懂 https://www.jianshu.com/p/38015afcdb58
来源: https://www.cnblogs.com/huansky/p/9656394.html