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源码,看看官方是怎么解析的。
/**
* 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 gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo 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.CustomLinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<org.dispatchtouchevent002.CustomButton
android: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);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
@Override
public 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);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public 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);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
}
1)首先我们来写三个View中的三个方法里的Log日志
RelativeLayout
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
return super.onTouchEvent(event);
}
LinearLayout
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomLinearLayout:dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomLinearLayout:onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TouchEvent", "CustomLinearLayout:onTouchEvent");
return super.onTouchEvent(event);
}
Button
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("TouchEvent", "CustomButton:dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TouchEvent", "CustomButton:onTouchEvent");
return super.onTouchEvent(event);
}
这里需要注意的是,在Activity中其实也含有
onInterceptTouchEvent()和
OnTouchEvent()这两个方法,所以我们也需要重写这两个方法。
MainActivity
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn=(Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TouchEvent", "MainActivity:onClick");
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "MainActivity:dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public 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
@Override
public 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
@Override
public 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:
@Override
public 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:
@Override
public 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:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
return true;
}
@Override
public 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() {
@Override
public void onClick(View v) {
Log.d("TouchEvent", "MainActivity:onClick");
}
});
btn.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TouchEvent", "MainActivity:onTouch");
return false;
}
});
RelativeLayout:onInterceptTouchEvent()返回false:不拦截事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");s
return 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() {
@Override
public 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 gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo 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 check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!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 now
mUnsetPressedState.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 away
setPressed(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 buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
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()里层层的判断,最终才会决定谁去消费这个事件。而这些层层的判断条件我已经写到图上去了,在这里不多做解析,想要继续深究的童鞋可以到源码中去找。
这次的浅谈就到此为止了,如果有问题的童靴欢迎一起探讨,互相学习,共同进步~