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()里层层的判断,最终才会决定谁去消费这个事件。而这些层层的判断条件我已经写到图上去了,在这里不多做解析,想要继续深究的童鞋可以到源码中去找。
这次的浅谈就到此为止了,如果有问题的童靴欢迎一起探讨,互相学习,共同进步~
作者:u011070603 发表于2016/9/28 23:26:40 原文链接
阅读:98 评论:0 查看评论