Android事件分发(一)

基于Android9.0,了解Android事件分发

还是那句话:点成线,线成面,切勿贪心,否则一脸懵逼

先记住这个事件分发的顺序:

Activity->ViewGroup->View

以及三个重要的方法:

方法名 作用是什么? 什么时候调用?
dispatchTouchEvent() 传递(分发)事件 当前View能够获取点击事件时
onTouchEvent() 处理点击事件 在dispatchTouchEvent()内部调用
onInterceptTouchEvent() 判断是否拦截事件
$\color{red}{注意:只存在于ViewGroup中}$
在ViewGroup的dispatchTouchEvent()中调用

脑海里大概有了这个顺序和概念,我们就从源码开始吧。

当触发点击事件时,最先响应的是

Activity的dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {//按下事件
        onUserInteraction();//这是一个空方法。activity无论分发按键事件、触摸事件或者轨迹球事件都会调用Activity#onUserInteraction()。 
    }
    if (getWindow().superDispatchTouchEvent(ev)) {//获取当前Window,Window是一个abstract类,这里它的实现是PhoneWindow
         //Window的superDispatchTouchEvent方法,也是一个abstract方法,所以要去看PhoneWindow的superDispatchTouchEvent
        return true;
    }
    return onTouchEvent(ev);//没有任何View 接收/处理事件,调用自身的onTouchEvent
}

PhoneWindow的superDispatchTouchEvent

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);//mDecor为DecorView实例,DecorView为PhoneWindow的顶层Window
}

DecorView继承于FrameLayout,而FrameLayout继承于ViewGroup,所以,DecorView是ViewGroup的子类。

//DecorView的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

这里super.dispatchTouchEvent(event);其实就是ViewGroup的dispatchTouchEvent。这里先不作深入分析,假设它返回false,先把第一条路走通,然后再来分析ViewGroup的dispatchTouchEvent。

最后调用onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

上面我们知道mWindow就是PhoneWindow,但是shouldCloseOnTouch不是abstract方法,它的实现在Window里

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;//点击Window外面
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {//peekDecorView为DecorView
        return true;
    }
    return false;
}

private boolean isOutOfBounds(Context context, MotionEvent event) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();
        final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();//16像素
        final View decorView = getDecorView();
        return (x < -slop) || (y < -slop)
                || (x > (decorView.getWidth()+slop))
                || (y > (decorView.getHeight()+slop));
    }

最后返回false,就代表没有消费事件。

Android事件分发(一)

ViewGroup的dispatchTouchEvent

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
     ...
        // 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) {
                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;
        }
        ...
             if (!canceled && !intercepted) {//没有取消也没有拦截
                  ...
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        ...
                        if (!child.canReceivePointerEvents()//是否响应
                                || !isTransformedTouchPointInView(x, y, child, null)) {//是否在View范围内
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);
                        ...
                         //注意dispatchTransformedTouchEvent方法,分发事件给子View的关键
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ...
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                             //在addTouchTarget里面,mFirstTouchTarget被赋值
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
。。。
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
...
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
             //注意dispatchTransformedTouchEvent方法,分发事件给子View的关键
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            ...
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    //注意dispatchTransformedTouchEvent方法,分发事件给子View的关键
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
               ...
            }
        }
		...
    }
...
    return handled;
}

先来看看onInterceptTouchEvent方法,最后再看dispatchTransformedTouchEvent这个方法

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;//默认返回false
}

既然ViewGroup默认不拦截,那就来看看dispatchTransformedTouchEvent是如何分发的

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {//子View为空,调用自己的dispatchTouchEvent
            handled = super.dispatchTouchEvent(event);
        } else {//子View不为空,调用子View的dispatchTouchEvent
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
...
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                ...
                handled = child.dispatchTouchEvent(event);
...
            }
            return handled;
        }
        ...
    } else {
        ...
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
         ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
...
    return handled;
}

其实逻辑不复杂,核心思想就是child==null,调用自身父类的dispatchTouchEvent,如果不为空,就调用子View的dispatchTouchEvent,接下来,我们就来看看View的dispatchTouchEvent。

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {//这里OnTouchListener,就是平常我们设置的setOnTouchListener,onTouch自然就是重写的onTouch
            result = true;
        }

        if (!result && onTouchEvent(event)) {//根据需求,可以重写onTouchEvent。onTouchEvent默认返回true
            result = true;
        }
    }...

    return result;
}

到这里,事件分发基本就结束了,是不是脑子有点糊?胖子第一次也是的,不过,加上自己写个demo,会清楚很多。下面我们来敲敲demo,加深这条线的印象。


首页创建MainActivity、CustomRelativeLayout、CustomView,然后分别重新他们的dispatchTouchEvent、onInterceptTouchEvent(CustomRelativeLayout独有)、onTouchEvent,加上日志,最后看看xml和CustomView的监听

<com.example.test.CustomRelativeLayout
    android:id="@+id/relativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.test.CustomView
        android:id="@+id/textTv"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true"
        android:background="@color/colorAccent" />

第一种情况:事件不消费

activityMainBinding.textTv.setOnTouchListener((v,event)->{
    Log.e(TAG,"textTv --->> setOnTouchListener");
    return false;//注意这里,默认是返回false。表示不消费
});
//        activityMainBinding.textTv.setOnTouchListener(new View.OnTouchListener() {
//            @Override
//            public boolean onTouch(View v, MotionEvent event) {
//                return false;
//            }
//        });

点击CustomView看看日志

E/MainActivity: 调用dispatchTouchEvent第0次 //点击,从上往下传递
E/MainActivity: dispatchTouchEvent   //先调用Activity的事件分发
E/CustomRelativeLayout: dispatchTouchEvent //再调用ViewGroup的事件分发
E/CustomRelativeLayout: onInterceptTouchEvent//判断ViewGroup是否拦截事件
E/CustomView: dispatchTouchEvent//View的dispatchTouchEvent
E/MainActivity: textTv --->> setOnTouchListener//View的setOnTouchListener,onTouch返回false
E/CustomView: onTouchEvent//从下往上返回
E/CustomView: onTouchEvent ACTION_DOWN
E/CustomRelativeLayout: onTouchEvent
E/CustomRelativeLayout: onTouchEvent ACTION_DOWN
E/MainActivity: 调用onTouchEvent第0次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_DOWN//最后到Activity的onTouchEvent,结束
E/MainActivity: 调用dispatchTouchEvent第1次//这里是抬起
E/MainActivity: dispatchTouchEvent
E/MainActivity: 调用onTouchEvent第1次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_UP

第二种情况:View消费

activityMainBinding.textTv.setOnTouchListener((v,event)->{
    Log.e(TAG,"textTv --->> setOnTouchListener");
    return true;
});
E/MainActivity:调用dispatchTouchEvent第0次//点击,从上往下传递
E/MainActivity:dispatchTouchEvent
E/CustomRelativeLayout:dispatchTouchEvent
E/CustomRelativeLayout:onInterceptTouchEvent
E/CustomView:dispatchTouchEvent
E/MainActivity:textTv--->>setOnTouchListener//这里返回true,就不往上返回了
E/MainActivity:调用dispatchTouchEvent第1次//这里是抬起
E/MainActivity:dispatchTouchEvent
E/CustomRelativeLayout:dispatchTouchEvent
E/CustomRelativeLayout:onInterceptTouchEvent
E/CustomView:dispatchTouchEvent
E/MainActivity:textTv--->>setOnTouchListener

第三种情况:ViewGroup拦截

CustomRelativeLayout
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    Log.e(TAG,"onInterceptTouchEvent");
    return true;//注意这里,返回true,拦截事件
}
E/MainActivity: 调用dispatchTouchEvent第0次
E/MainActivity: dispatchTouchEvent
E/CustomRelativeLayout: dispatchTouchEvent
E/CustomRelativeLayout: onInterceptTouchEvent//这里返回true,拦截事件
E/CustomRelativeLayout: onTouchEvent//直接调用自己的onTouchEvent
E/CustomRelativeLayout: onTouchEvent ACTION_DOWN
E/MainActivity: 调用onTouchEvent第0次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_DOWN
E/MainActivity: 调用dispatchTouchEvent第1次
E/MainActivity: dispatchTouchEvent
E/MainActivity: 调用onTouchEvent第1次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_UP

第四种情况:ViewGroup消费

activityMainBinding.relativeLayout.setOnTouchListener((v,event)->{
    Log.e(TAG,"relativeLayout --->> setOnTouchListener");
    return true;
});

与情况二类似,onTouch返回true,就不再往上传递了。


关于总结:还是不借鉴各路大神的blog总结了。胖子觉得这样会造成部分朋友只看总结,不看内容,最后变成知其然不知其所以然(胖子就吃过很多这样的亏),还是交给朋友们自行总结吧。

胖子总结

  • 先清楚传递Activity->ViewGroup->View和返回View->ViewGroup->Activity顺序
  • 再搞清楚dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()的基本作用
  • 打开源码,一步一步跟下去,自己用工具画一画它们之间的关系,之后脑袋里就会有大致概念了
  • 温馨提示:点成线,线成面,切勿贪心,否则一脸懵逼
  • 胖子有什么理解错误的,欢迎大家指出来,一起讨论、学习、进步
  • 期待胖子的第三篇《Android事件分发(二)》

参考文献

图解 Android 事件分发机制

Android事件分发机制详解:史上最全面、最易懂

上一篇:妈妈再也不用担心我的面试,顺利收获Offer


下一篇:Android事件分发机制