前言
说到这个事件分发机制呢,我觉得一直以来都是我的弱项,可能它太抽象了,也与我在实际项目中没怎么使用到过,也没自定义过view有着很大的关系。虽然在面试过程中事件分发是必不可少要问的,但是我也是一知半解的仅能说一点点。所以决定接下来好好学习下这部分知识,试着去尝试自定义一些View。
一、为什么要引入事件分发机制?
①android上的View是树形结构的,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢。
②Activity是如何响应对其中的某个视图的触摸操作的?
③Activity是如何响应对手机的按键操作的?
为了解决这些问题而引入了事件分发机制。
二、事件分发机制基础认知
1.MotionEvent事件的4种类型
MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间。
MotionEvent.ACTION_MOVE:手指在屏幕上移动
MotionEvent.ACTION_UP:手指离开屏幕瞬间
MotionEvent.ACTION_CANCEL:取消手势
2.onTouch是优先于onClick执行,事件传递的顺序是先经过onTouch,再传递到onClick。
3.Android中的事件onClick、onLongClick、onScroll等,都是由多个Touch事件(一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP)组成。
4.Android事件分发是从外向里分发,事件处理是从里向外。
5.事件序列:从手指接触屏幕的一瞬间起,直到手指从屏幕上松开的一瞬间所产生的一切事件。任何事件都是从Down事件开始,UP事件结束,中间有无数的MOVE事件。当一个MotionEvent产生之后,系统需要把这个事件传递给一个具体的view去处理。
6.事件分发的本质:将MotionEvent事件传递给一个具体的view进行处理。
7.事件在哪些对象之间进行传递? Activity ViewGroup View
补充:View和ViewGroup的区别?
View类是所有视图(包括ViewGroup)的根基类。
ViewGroup包含一些View或ViewGroup,用户控制子View的布局。
ViewGroup是View的子类,也就是说ViewGroup本身就是一个View,但是它可以包含子View(当然子View也可能是一个ViewGroup)。ViewGroup 处理事件分发的流程,而View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。
8.事件分发机制的三个过程,事件分发——>事件拦截——>事件响应
(1)事件分发:将事件传递给某个具体的View,实际上就是点击事件由哪个对象发出,经过哪些对象,最终要达到哪个对象,并最终得到处理的过程。
事件分发的顺序:由外向里分发,Activity----ViewGroup----View
事件分发调用的方法:
dispatchTouchEvent:当点击事件能够传递给当前的View时,调用该方法,负责将事件分发到其子view或者当前view中。
返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法
onTouchEvent:完成对点击事件的处理
真正对MotionEvent进行处理或者说消费的方法。在dispatchTouchEvent进行调用。
返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法
onInterceptTouchEvent:拦截点击事件(只存在于ViewGroup中)优先级高于onTouchEvent
事件拦截,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是ViewGroup特有的方法,View并没有拦截方法
返回值:是否拦截事件传递,返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。返回false表示不做拦截,事件将向下分发到子View的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方法
}
三、Activity的事件分发机制
当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent()进行事件分发机制。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {//由于事件一般都是为down事件(按下)所以一般都会调用该方法
onUserInteraction();
}
//若getWindow().superDispatchTouchEvent(ev)返回true,则dispatchTouchEvent(MotionEvent ev)就返回true,停止事件传递
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev); //否则继续往下调用Activity.onTouchEvent
}
public void onUserInteraction() {
}
//是个空方法,当触摸点击home,back,menu键的时候回触发该方法,为了实现屏保功能
接着分析getWindow().superDispatchTouchEvent(ev)
getWinsow()=获取Window类的对象
Window类是个抽象类,其唯一实现类=PhoneWindow类;即此处的Window类对象=PhoneWindow对象
Window类的superDispatchTouchEvent()这个抽象方法,由子类PhoneWindow实现
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//这里的mDecor指的是顶层view(DecorView对象)
DecorView类是PhoneWindow类的一个内部类。继承自FrameLayout,相当于间接的继承自ViewGroup。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event); //调用父类ViewGroup的dispatchTouchEvent()
//即将事件传递给ViewGroup去处理。
}
接着分析下Activity的onTouchEvent()方法
//当一个点击事件未被Activity下的任何一个View接收、处理时,会调用Activity的onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false; //当点击事件在Window边界内时,返回false
}
//分析mWindow.shouldCloseOnTouch()
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
//用于处理边界外点击事件的判断,是否是Down事件,event的坐标是否在边界内等。
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true; //返回true:说明事件在边界外,即消费事件
}
return false;//返回false:未消费
}
总结一下Activity的事件分发机制:当一个点击事件发生时,首先传给Activity的dispathTouchEvent()。当Activity下有View进行处理,就需要将事件进行一个传递!
那事件是如何由Activity传递给ViewGroup的呢?首先是传给Window(由于Window是个抽象类,它的唯一实现类是PhoneWindow也就是说传给了PhoneWindow的dispatchEvent),再接着又传给了DecorView的dispatchEvent(DecorView是顶层View,它继承自FrameLayout,由于FrameLayout是ViewGroup的子类,所以ViewGroup也是DecorView的间接父类)至此,就实现了Activity将事件传递给ViewGroup的过程。
当事件未被Activity下的任何一个View接收时,就需要调用Activity自身的onTouchEvent()方法。
四、ViewGroup的事件分发机制
ViewGroup的事件分发机制从dispatchTouchEvent()开始【负责将事件传递到子view中】,贴下关键的代码:
final boolean intercepted; //是否拦截事件
//mFirstTouchTarget ,如果事件由子view处理时mFirstTouchTarget会被赋值并指向子View
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT是子类通过requestDisallowInterceptTouchEvent方法进行设置的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//调用onInterceptTouchEvent方法判断是否需要拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false; //不禁用
}
} else {
intercepted = true; //禁用
}
当ViewGroup决定拦截事件后,后续事件将默认交给它处理,并且并且不会再调用onInterceptTouchEvent方法来判断是否拦截。子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。
当ViewGroup不拦截事件,那么事件将下发给子view进行处理。接着源码中做了一个for循环,通过倒序遍历ViewGroup下面所有的子View,然后一个一个判断点击位置是否是该子view的布局区域。如果是调用该View的dispatchTouchEvent(),即实现了点击事件从ViewGroup到子View的传递。如果是点击的是空白处,(就是没有任何View接收事件时 拦截事件需手动复写onInterceptTouchEvent)
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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 there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
总结ViewGroup的事件分发机制:当点击事件传递给ViewGroup的时候,也就是调用ViewGroup的dispatchTouchEvent,然后在其内部调用onInterceptTouchEvent()如果返回true(当前事件被ViewGroup拦截)事件停止往下传递,ViewGroup自己处理事件,调用父类的dispatchTouchEvent(),最终执行自己的onTouchEvent()。 如果返回false(当前事件没有被ViewGroup拦截)默认设置,事件继续往下传递,事件传到子view,即调用子View的dispatchTouchEvent()处理。
onTouchEvent:true ViewGroup处理了当前事件,事件分发结束,逐层返回true结束。 false:ViewGroup不处理事件,事件将抛给上层的Activity的onTouchEvent处理。
五、View的事件分发机制
ViewGroup归根结底也是一个view
必须满足三个条件都为真,才会返回true。
①mOnTouchListener不为null,即调用了setOnTouchListener();
②(mViewFlags & ENABLED_MASK) == ENABLED该条件是判断当前点击的控件是否为 enable,但由于基本 View 都是 enable 的,所以这个条件基本都返回 true。
③li.mOnTouchListener.onTouch(this, event))即我们调用 setOnTouchListener()
时必须覆盖的方法 onTouch()
的返回值。终于知道onTouch()方法优先级高于onTouchEvent(event)方法是怎么来的了。
由于我们在View中常用的两个方法onTouchListener和onClick,在dispatchTouchEvent中会首先判断onTouchListener是否为空,如果不为空。会直接处理。如果为空,就调用onTouchEvent方法,在调用onClick方法。因此在View中onTouchListener的优先级i高于onClick,同时一个手势操作最多只能被其中一个处理。
View事件分发流程图
子view可以通过requestDisallowInterceptTouchEvent方法干涉父View的事件分发过程(ACTION_DOWN事件除外)。
对于View,如果设置了onTouchListener,那么onTouchListener方法中的onTouch方法会被回调。onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)所以三者的优先级是onTouch->onTouchListener->onClick
举例分析事件的分发和处理的过程。