Android事件分发机制浅谈
前言:可能Android的事件分发对于刚学Android的童鞋来说接触得不多,这样不奇怪。因为刚学的时候,一般人很难注意到或是会选择主动去了解。那么究竟什么是Android的事件分发呢?

或许刚说出来,有点觉悟的新手会想到就是那些按钮的点击事件、或是说监听。而这些也确实是Android事件分发的其中一部分。由于Android的事件分发其实是可以有很多变化的,特别是当你需要自定义View的时候,很多情况都需要具体分析,所以大体上它都不容易精确的掌握。但如果是主流的,大概的事件分发机制其实也没那么难理解,说到底这可以说是一个浅入深出的问题吧。那么接下来我们来浅谈一下这次的主题Android事件分发机制。

1.比喻
打个比方,Android的事件分发中的事件(用户的触屏)就像一块饼。假若有一家3代的人在饥荒的年代里,如果爷爷有一块饼,那么他会先给谁?那当然会是孙子。但爷爷年纪大视力不好,所以他把饼传递给了儿子,而儿子又把这块饼传给了孙子,最终由孙子吃下了那块饼。而像这样的父穿子,子传孙的方法,就如同我们Android中的事件分发一样。
接下来我们新建一个工程并写好如上图的布局,这是一个最外层包着RelativeLayout(爷爷),中间是LinearLayout(儿子),最里面是Button(孙子)。
图画得不是很好,大家就凑合这看吧。上面就是我们布局的上个View。而触摸事件(饼)就是通过RelativeLayout--->LinearLayout--->Button,这样一层层的传递的。而事件的分发就是这样通过disppatchTouchEvent(),OnInterceptTouchEvent(),OnTouchEvent()这三个方法去判断事件是否继续向下传递。
而当其中有一层View自己先截获了事件消费掉了(可理解为自己用掉了点击事件),那么事件则不会向下传递,而在OnTouchEvent中返回False则不会自己消费并返回到上一层去处理。
看到这里大家肯定还是不明白,接着到java代码里面,按着 Ctrl+ Shift+ T,查找一下View的API,这里我选API23的,其实API多少都没太大变化,我就选一个最新的。
3.理解dispatchTouchEvent(),onInterceptTouchEvent(),OnTouchEvent()三个事件
这三个事件理解起来,首先要区分View与ViewGroup两种情况:
* View:dispatchTouchEvent,OnTouchEvent
* ViewGroup:dispatchTouchEvent,onInterceptTouchEvent,OnTouchEvent
其中View是没有onInterceptTouchEvent方法的。在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法,所以我们会找View的API,而不是找Button的API,因为这个方法一般在具体的控件类中是找不到的,他是父View的方法。
下面我贴出找到的dispatchTouchEvent源码,看看官方是怎么解析的。

理解dispatchTouchEvent()

/*** Pass the touch screen motion event down to the target view, or this* view if it is the target.** @param event The motion event to be dispatched.* @return True if the event was handled by the view, false otherwise.*/public boolean dispatchTouchEvent(MotionEvent event) {// If the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) {// We don't have focus or no virtual descendant has it, do not handle the event.if (!isAccessibilityFocusedViewOrHost()) {return false;}// We have focus and got the event, then use normal event dispatch.event.setTargetAccessibilityFocus(false);}boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// Clean up after nested scrolls if this is the end of a gesture;// also cancel it if we tried an ACTION_DOWN but we didn't want the rest// of the gesture.if (actionMasked == MotionEvent.ACTION_UP ||actionMasked == MotionEvent.ACTION_CANCEL ||(actionMasked == MotionEvent.ACTION_DOWN && !result)) {stopNestedScroll();}return result;}
看上面的官方解析,大概意思是:这个View接收的是从屏幕传递过来的事件并传给目标的View,或是这个View就是从屏幕传达过来的事件的目标。
这句话怎么理解呢?其实也就对应我上面画的图,两种情况:要么该View就是目标的View(自己消费掉事件),要么向下传递事件。而这个方法默认是调用super.dispatchTouchEvent(event)的,需传入super.dispatchTouchEvent(true),需要注意的是他如果直接返回true或flase而不是进过调用supper的都不会向下传递。这里可能有点难理解,不用急,下面会继续解析。
接着我们看看onInterceptTouchEvent()方法。
理解onInterceptTouchEvent()
同样的快捷键方法我们来到ViewGroup的源码:
/*** Implement this method to intercept all touch screen motion events.  This* allows you to watch events as they are dispatched to your children, and* take ownership of the current gesture at any point.** <p>Using this function takes some care, as it has a fairly complicated* interaction with {@link View#onTouchEvent(MotionEvent)* View.onTouchEvent(MotionEvent)}, and using it requires implementing* that method as well as this one in the correct way.  Events will be* received in the following order:** <ol>* <li> You will receive the down event here.* <li> The down event will be handled either by a child of this view* group, or given to your own onTouchEvent() method to handle; this means* you should implement onTouchEvent() to return true, so you will* continue to see the rest of the gesture (instead of looking for* a parent view to handle it).  Also, by returning true from* onTouchEvent(), you will not receive any following* events in onInterceptTouchEvent() and all touch processing must* happen in onTouchEvent() like normal.* <li> For as long as you return false from this function, each following* event (up to and including the final up) will be delivered first here* and then to the target's onTouchEvent().* <li> If you return true from here, you will not receive any* following events: the target view will receive the same event but* with the action {@link MotionEvent#ACTION_CANCEL}, and all further* events will be delivered to your onTouchEvent() method and no longer* appear here.* </ol>** @param ev The motion event being dispatched down the hierarchy.* @return Return true to steal motion events from the children and have* them dispatched to this ViewGroup through onTouchEvent().* The current target will receive an ACTION_CANCEL event, and no further* messages will be delivered here.*/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;}
这里他的代码不是很多,但官方的注释却又很多,我们只看第一段,大概意思是:这个方法他拦截了所有的屏幕事件。他允许用户监测这些事件,并且可以分发到他的子View里面,作为子View的一个手势(或是说任何点)的手势。
所以从上面我们着重的可以知道,onInterceptTouchEvent()这个方法可以拦截事件的分发。而当onInterceptTouchEvent()返回的是false的时候就说明不拦截这个View的子View,那么子View就可以获取到这个事件了。
而OnTouchEvent()方法在View跟ViewGroup都有,所以要分开讨论。我们现在可以理解为,若是OnTouchEvent()返回true则代表这一层的View自己消费掉事件,而返回false,那么事件会重新返回到该View的父View,也就是上一层的View中。
当RelativeLayout在onInterceptTouchEvent()里面不拦截子View的时候,事件就会传递到LinearLayout的dispatchTouchEvent()事件里面。而同样LinearLayout也是继承ViewGroup的,所以他也有onInterceptTouchEvent方法。
而LinearLayout的OnTouchEvent()里面,如果返回true则代表自己消费掉事件,而如果返回false则表示不作处理并返回给上层父View处理
4.结合例子理解
这个是我编写的上图的布局,里面三个都是简单的自定义View,作用是方便打印出信息
<org.dispatchtouchevent002.CustomRelativieLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="org.heima.dispatchtouchevent002.MainActivity" ><org.dispatchtouchevent002.CustomLinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical" ><org.dispatchtouchevent002.CustomButtonandroid:id="@+id/btn"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="click Me" /></org.dispatchtouchevent002.CustomLinearLayout>
</org.dispatchtouchevent002.CustomRelativieLayout>
接下来我们在这些View里面重写dispatchTouchEvent()和OnTouchEvent()两个方法,分别在里面打印Log,查看结果。
RelativeLayout
public class CustomRelativieLayout extends RelativeLayout {public CustomRelativieLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public CustomRelativieLayout(Context context, AttributeSet attrs) {super(context, attrs);}public CustomRelativieLayout(Context context) {super(context);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return false;}@Overridepublic boolean onTouchEvent(MotionEvent event) {return super.onTouchEvent(event);}
}

LinearLayout

public class CustomLinearLayout extends LinearLayout{public CustomLinearLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public CustomLinearLayout(Context context, AttributeSet attrs) {super(context, attrs);}public CustomLinearLayout(Context context) {super(context);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {return super.onTouchEvent(event);}
}

Button

public class CustomButton extends Button{public CustomButton(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public CustomButton(Context context, AttributeSet attrs) {super(context, attrs);}public CustomButton(Context context) {super(context);}@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);}@Overridepublic boolean onTouchEvent(MotionEvent event) {return super.onTouchEvent(event);}
}
1)首先我们来写三个View中的三个方法里的Log日志
RelativeLayout
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");return false;}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");return super.onTouchEvent(event);}
LinearLayout
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomLinearLayout:dispatchTouchEvent");return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomLinearLayout:onInterceptTouchEvent");return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.d("TouchEvent", "CustomLinearLayout:onTouchEvent");return super.onTouchEvent(event);}

Button

@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {Log.d("TouchEvent", "CustomButton:dispatchTouchEvent");return super.dispatchTouchEvent(event);}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.d("TouchEvent", "CustomButton:onTouchEvent");return super.onTouchEvent(event);}
这里需要注意的是,在Activity中其实也含有onInterceptTouchEvent()和OnTouchEvent()这两个方法,所以我们也需要重写这两个方法。
MainActivity
public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button btn=(Button) findViewById(R.id.btn);btn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {Log.d("TouchEvent", "MainActivity:onClick");}});}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "MainActivity:dispatchTouchEvent");return super.dispatchTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.d("TouchEvent", "MainActivity:onTouchEvent");return super.onTouchEvent(event);}
}
之后我们按下按钮。因为点击事件是由按下和抬起两部分组成的,所以上述的Log日志会打印两次。在Android里面,按下和抬起是分别处理的两个不同的事件,可以看到打印结果如下:
按下
09-27 17:27:41.222: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:onTouchEvent

抬起

09-27 17:27:41.321: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:onTouchEvent
09-27 17:27:41.331: D/TouchEvent(1493): MainActivity:onClick
总结1:
配合我画的图,结合上面的Log日志可以看出,点击事件最终被Button的onClick事件所消费。
MainActivity:
dispatchTouchEvent()为默认,向下传递。
Relative:
dispatchTouchEvent()为默认true,向下传递。
onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。
LinearLayout:
dispatchTouchEvent()为默认true,向下传递。
onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。
Button:
dispatchTouchEvent()为默认true,向下传递。
OnTouchEvent():为默认false,默认不处理。
事件到了Button的OnTouchEvent()里面后,由于默认是false,OnTouchEvent()方法不处理。,又向最上层的父View返回了,
看到这里的Log日志再配合上面的图,是不是应该能稍微理解了一些呢?若是不了解的话,那还得自己多看几遍,或是自己也试试打印Log测试一下了。
2)接着我们来讨论上面理解dispatchTouchEvent()时出现的一种情况:dispatchTouchEvent()返回true或false時不向下传递事件,当只有调用super.dispatchTouchEvent()的时候才会。
在这里我试着修改RelativeLayout里面的dispatchTouchEvent(),其余布局不变
RelativeLayout:dispatchTouchEvent()返回true
 @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");return true;}
Log日志:
09-28 01:23:09.349: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.349: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent

RelativeLayout:dispatchTouchEvent()返回false

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");return false;}

Log日志:

09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): CustomRelativieLayout:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:onTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:onTouchEvent
总结2:
当dispatchTouchEvent()返回true或者flase的时候,事件不向下传递,只有返回的是 super.dispatchTouchEvent()才会进行传递。并且返回false的时候事件会返回到上层View的onTouchEvent(),但如果父View的onTouchEvent为默认(默认不处理)。那么最终这个事件会没有响应,不被任何层的View所消费掉。
3)接下来我们调用super.dispatchTouchEvent(),但不把他返回,而是选择返回true或者false,观察情况如何:
RelativeLayout:调用super.dispatchTouchEvent(),返回true:
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");boolean event = super.dispatchTouchEvent(ev);Log.d("TouchEvent", "Touch:"+event);return true;}

Log日志:

09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:onClick

RelativeLayout:调用super.dispatchTouchEvent(),返回false:
 @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");boolean event = super.dispatchTouchEvent(ev);Log.d("TouchEvent", "Touch:"+event);return false;}

Log日志:

09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:onTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): Touch:true
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:onTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:onTouchEvent
总结3:
从日志来看,当有调用super.dispatchTouchEvent()并且返回true的情况下,跟我们结论1里,直接返回super.dispatchTouchEvent()的结果是一样的。
但当super.dispatchTouchEvent()并且返回false时,事件走到一半就停止了,看到日志的第8行后ANCTION_DOWN(点下去)已经执行完了,而抬起来的事件只走到Activity里面的dispatchTouchEvent()就再也没有分发下去。

4)接着onInterceptTouchEvent()返回true,则为拦截事件的分发。
RelativeLayout:onInterceptTouchEvent()返回ture:
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");return true;}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");boolean touch = super.onTouchEvent(event);Log.d("TouchEvent", "onToucheEvent:"+touch);return super.onTouchEvent(event);}

Log日志:

09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onInterceptTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): onToucheEvent:false
09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:onTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:onTouchEvent
结4:
这里,我在RelativeLayout里面的OnTouchEvent()方法打印了super.onTouchEvent的值,可以看到当为false。当onInterceptTouchEvent()为true时,事件不分发给下一层的子View,而选择走自己的onToucheEvent()方法,但又是默认的false不处理。导致事件回到上一层的父View中,最终父View的onTouchEvent()也是默认为false,不处理事件。最后导致事件没有任何View响应,也就没有消费。
5)在Activity里,给Button设置OnTouchListener,在onTouch()中返回false,默认不拦截。
MainActivity:onTouch()返回false:默认不拦截
 btn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {Log.d("TouchEvent", "MainActivity:onClick");}});btn.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {Log.d("TouchEvent", "MainActivity:onTouch");return false;}});

RelativeLayout:onInterceptTouchEvent()返回false:不拦截事件

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");return true;}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");sreturn super.onTouchEvent(event);}

Log日志:

09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onClick
总结5:
从上面的结果来看,事件在传递到Button的disptchTouchEvent(),然后回到Activity调用自己的onTouch(),dispatchTouchEvent结束后调用Button自己的onTouchEvent()(第8行),到此按下去的事件已经完成。
紧接着到抬起来的up事件。而执行的Log结果大致上也跟按下去的down事件差不多,但不同的是,up事件最终会在Button自己的OnTouchEvent()中响应,而onTouchEvent()会调用Buttton自己的onClick()方法,最后由onClick方法消费。
6)Button的onTouch()方法返回true
MainActivity:onTouchEvent返回true
btn.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {Log.d("TouchEvent", "MainActivity:onTouch");return true;}});

Log日志:

09-28 04:23:15.712: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.722: D/TouchEvent(2223): MainActivity:onTouch
09-28 04:23:15.882: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.892: D/TouchEvent(2223): MainActivity:onTouch
总结6:
可以看到onTouch()设置为true,事件到最后也没传递到onClick()里面,而是由Button的onTouch()给消费了。所以onTouch()方法应该是先于onClick执行的,事件到了onTouch()(返回true)已经被消费了,那么整个方法都已经返回了,事件就不会进一步传递,自然没有onClcik的事。
到这里我们差不多可以结合源码来解析一下我们锁看到的现象了。
7.dispathTouchEvent()和onTouch()源码理解
找到View中dispathTouchEvent()的源码:
public boolean dispatchTouchEvent(MotionEvent event) {// If the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) {// We don't have focus or no virtual descendant has it, do not handle the event.if (!isAccessibilityFocusedViewOrHost()) {return false;}// We have focus and got the event, then use normal event dispatch.event.setTargetAccessibilityFocus(false);}boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// Clean up after nested scrolls if this is the end of a gesture;// also cancel it if we tried an ACTION_DOWN but we didn't want the rest// of the gesture.if (actionMasked == MotionEvent.ACTION_UP ||actionMasked == MotionEvent.ACTION_CANCEL ||(actionMasked == MotionEvent.ACTION_DOWN && !result)) {stopNestedScroll();}return result;}
从上面的代码24行开始可以看出dispathToychEvent()若能返回true的话都是要在,第24行或是第38行的判断中返回true,dispathTouchEvent才会为true。在这里其实可以看到前面的第24行的判断正是onTouch()方法的响应。而我们onTouch()方法的返回值就是写在Acivity中的代码,所以当onTouch()返回true的时候,事件在这里就已经消费了,而dispathToychEvent()也马上返回false。
而如果onTouch()返回false则事件会去到View自己的OnTouchEvent()方法里,那么onClick方法是怎么才调用到的呢?接着我们再看源码。
找到View中dispathTouchEvent()的源码:
public boolean onTouchEvent(MotionEvent event) {final float x = event.getX();final float y = event.getY();final int viewFlags = mViewFlags;final int action = event.getAction();if ((viewFlags & ENABLED_MASK) == DISABLED) {if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}// A disabled view that is clickable still consumes the touch// events, it just doesn't respond to them.return (((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);}if (mTouchDelegate != null) {if (mTouchDelegate.onTouchEvent(event)) {return true;}}if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {switch (action) {case MotionEvent.ACTION_UP:boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {// take focus if we don't have it already and we should in// touch mode.boolean focusTaken = false;if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {focusTaken = requestFocus();}if (prepressed) {// The button is being released before we actually// showed it as pressed.  Make it show the pressed// state now (before scheduling the click) to ensure// the user sees it.setPressed(true, x, y);}if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {// This is a tap, so remove the longpress checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!focusTaken) {// Use a Runnable and post this rather than calling// performClick directly. This lets other visual state// of the view update before click actions start.if (mPerformClick == null) {mPerformClick = new PerformClick();}if (!post(mPerformClick)) {performClick();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.run();}removeTapCallback();}mIgnoreNextUpEvent = false;break;case MotionEvent.ACTION_DOWN:mHasPerformedLongPress = false;if (performButtonActionOnTouchDown(event)) {break;}// Walk up the hierarchy to determine if we're inside a scrolling container.boolean isInScrollingContainer = isInScrollingContainer();// For views inside a scrolling container, delay the pressed feedback for// a short period in case this is a scroll.if (isInScrollingContainer) {mPrivateFlags |= PFLAG_PREPRESSED;if (mPendingCheckForTap == null) {mPendingCheckForTap = new CheckForTap();}mPendingCheckForTap.x = event.getX();mPendingCheckForTap.y = event.getY();postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());} else {// Not inside a scrolling container, so show the feedback right awaysetPressed(true, x, y);checkForLongClick(0);}break;case MotionEvent.ACTION_CANCEL:setPressed(false);removeTapCallback();removeLongPressCallback();mInContextButtonPress = false;mHasPerformedLongPress = false;mIgnoreNextUpEvent = false;break;case MotionEvent.ACTION_MOVE:drawableHotspotChanged(x, y);// Be lenient about moving outside of buttonsif (!pointInView(x, y, mTouchSlop)) {// Outside buttonremoveTapCallback();if ((mPrivateFlags & PFLAG_PRESSED) != 0) {// Remove any future long press/tap checksremoveLongPressCallback();setPressed(false);}}break;}return true;}return false;}
代码很多,我们看到第24行,程序进过一些列的判断后会进入一个 switch()语句判断,而里面的case事件就是我们熟悉的ACTION_DOWN,ACTION_UP,ACTION_MOVE等事件的处理。
而我们的setOnClickListener在哪?Ctrl+F搜索一下,发现下面两段代码:
 public void setOnClickListener(@Nullable OnClickListener l) {if (!isClickable()) {setClickable(true);}getListenerInfo().mOnClickListener = l;}

其中有一个变量mOnClickListener.于是再搜索:

public boolean performClick() {final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffectConstants.CLICK);li.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);return result;}

到这里我们可以知道performClick()方法是在OnTouchEvent()里面的ACTION_UP调用的。所以Android的事件分发都是这样进过一层层的View,再通过每个View中的dispatchTouchEvent()和onTouchEvent()里层层的判断,最终才会决定谁去消费这个事件。而这些层层的判断条件我已经写到图上去了,在这里不多做解析,想要继续深究的童鞋可以到源码中去找。


这次的浅谈就到此为止了,如果有问题的童靴欢迎一起探讨,互相学习,共同进步~


Android事件分发浅谈相关推荐

  1. 浅谈Android事件分发机制

    在Android实际开发过程中经常会遇到View之间的滑动冲突,如ScrollView与Listview.RecyclerView之间的嵌套使用.在很好的解决此类问题之前,我们应深入的了解Androi ...

  2. Android事件分发-来龙去脉

    文章目录 情境(Situation) 冲突(Complication) 疑问(Question) 答案(Answer) 剖析 论点 约法三章 点 论据 人机交互 View树 类图 注释 DecorVi ...

  3. Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

    <div id="container">         <div id="header">     <div class=&qu ...

  4. Android 事件分发 简单学

    为什么80%的码农都做不了架构师?>>>    本文地址:https://my.oschina.net/lifj/blog/1928132 Android 事件分发过程 网上有很多这 ...

  5. android触摸事件分发,Android 事件分发机制

    Android 事件分发机制一直让人头痛,之前也是面向 GitHub 编程得过且过.今天下定决心了解一下,以便后面自己定制 View 效果.Android 触摸事件有三个基本类型:ACTION_DOW ...

  6. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  7. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  8. 【Android 事件分发】ItemTouchHelper 事件分发源码分析 ( 绑定 RecyclerView )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  9. 【Android 事件分发】ItemTouchHelper 实现拖动排序

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

最新文章

  1. Ajax PHP 边学边练 之四 表单
  2. Darwin Stream server(DSS服务器)的Relay(中继/转发)设置
  3. maven实战总结,工作中常见操作
  4. js 数组过滤_JS之 开发技巧
  5. Android学习笔记---24_网络通信之网页源码查看器
  6. ensp 双机热备 配置_华为交换机VRRP配置教程(一)
  7. 如何把HTML背景图片变透明,photoshop怎样把图片背景变透明
  8. 笛卡尔心形函数表达式_如何用几何画板画笛卡尔心形函数
  9. 怎么在WORD里给文字“框”起来,干货在这里,WORD文档中文字加边框的独家教程
  10. 福建广电网络显示服务器异常,无法浏览网页故障
  11. 云原生的进一步具象化
  12. 【Unity实战100例】Unity屏幕画线,Unity屏幕画图HSJ绘画工具
  13. CBAM CBAM: Convolutional Block Attention Module
  14. 大数据运维 | 集群_监控_CDH_Docker_K8S_两项目_云服务器
  15. 2014年暑假——英语清凉了夏季的炎热
  16. 关于 SetProcessWorkingSetSize 和内存释放
  17. echarts自定义组件
  18. 小样本学习元学习经典论文整理||持续更新
  19. 虚拟机当服务器的设置,虚拟机当作设置服务器
  20. string转double java_Java String转double

热门文章

  1. NRF24L012.4G模块
  2. OGG遇到相关问题汇总
  3. JDBC及操作数据库步骤
  4. mysql中如何查看表结构
  5. ENSP华为模拟器:基础命令及简写
  6. 算法刷题 -- 1823. 找出游戏的获胜者<难度 ★★☆>
  7. JavaSE:抽象(abstract)
  8. 机器学习的可解释性(总结)
  9. 单片机数字滤波c语言程序,单片机系统中数字滤波的算法【C程序整理】
  10. ctf之php序列化,0ctf_2016_unserialize(php反序列化逃逸字符)