版权声明:本文出自汪磊的博客,转载请务必注明出处。
在前两篇我们共同探讨了事件传递机制《View篇》与《ViewGroup篇》,我们知道View触摸事件是ViewGroup传递过去的,比如一个很简单的布局最外层是LinearLayout,里面就一个Button,我们点击Button的时候触摸事件是由外层LinearLayout传递给里面Button的,但是有没有想过当前触摸事件是谁传递给外层的LinearLayout的呢?带着这个疑问我们继续来共同探讨一下。
从Demo示例说起
我们还是先写一个简单的demo,很简单,代码如下:自定义Button:
1 public class MyButton extends Button { 2
3 private final String TAG = "WL"; 4
5 public MyButton(Context context, AttributeSet attrs) { 6 super(context, attrs); 7 } 8
9 @Override 10 public boolean dispatchTouchEvent(MotionEvent ev) { 11 // 12 Log.i(TAG, "MyButton_dispatchTouchEvent_Action:"+ev.getAction()); 13 return super.dispatchTouchEvent(ev); 14 } 15
16 @Override 17 public boolean onTouchEvent(MotionEvent event) { 18 // 19 Log.i(TAG, "MyButton_onTouchEvent_Action:"+event.getAction()); 20 return super.onTouchEvent(event); 21 } 22 }
自定义LinearLayout:
1 public class MyLinearLayout extends LinearLayout { 2
3 private final String TAG = "WL"; 4
5 public MyLinearLayout(Context context, AttributeSet attrs) { 6 super(context, attrs); 7 // 8 } 9
10 @Override 11 public boolean dispatchTouchEvent(MotionEvent ev) { 12 // 13 Log.i(TAG, "MyLinearLayout_dispatchTouchEvent_Action:"+ev.getAction()); 14 return super.dispatchTouchEvent(ev); 15 } 16
17 @Override 18 public boolean onTouchEvent(MotionEvent event) { 19 // 20 Log.i(TAG, "MyLinearLayout_onTouchEvent_Action:"+event.getAction()); 21 return super.onTouchEvent(event); 22 } 23 }
布局文件:
1 <com.wl.activitydispatchtouchevent.MyLinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 android:background="#0099cc"
7 android:id="@+id/mylinearlayout"
8 android:gravity="center"
9 tools:context="com.wl.activitydispatchtouchevent.MainActivity" >
10
11 <com.wl.activitydispatchtouchevent.MyButton 12 android:id="@+id/mybutton"
13 android:layout_width="wrap_content"
14 android:layout_height="wrap_content"
15 android:textSize="20sp"
16 android:text="WL_Button" />
17
18 </com.wl.activitydispatchtouchevent.MyLinearLayout>
Activity中代码:
1 public class MainActivity extends Activity implements OnClickListener, 2 OnTouchListener { 3
4 private final String TAG = "WL"; 5
6 @Override 7 protected void onCreate(Bundle savedInstanceState) { 8 super.onCreate(savedInstanceState); 9 setContentView(R.layout.activity_fullscreen); 10 // 11 findViewById(R.id.mybutton).setOnClickListener(this); 12 findViewById(R.id.mybutton).setOnTouchListener(this); 13 // 14 findViewById(R.id.mylinearlayout).setOnClickListener(this); 15 findViewById(R.id.mylinearlayout).setOnTouchListener(this); 16 } 17
18 @Override 19 public boolean onTouch(View v, MotionEvent event) { 20 // 21 Log.i(TAG, "onTouch___v:" + v + "___action:" + event.getAction()); 22 return false; 23 } 24
25 @Override 26 public void onClick(View v) { 27 // 28 Log.i(TAG, "onClick___v:" + v); 29 } 30
31 @Override 32 public boolean dispatchTouchEvent(MotionEvent ev) { 33 Log.i(TAG, "MainActivity__dispatchTouchEvent__action:" + ev.getAction()); 34 return super.dispatchTouchEvent(ev); 35 } 36
37 @Override 38 public boolean onTouchEvent(MotionEvent event) { 39 Log.i(TAG, "MainActivity___onTouchEvent___action=" + event.getAction()); 40 return super.onTouchEvent(event); 41 } 42 }
怎么样,很简单吧。和上一篇讲解ViewGroup传递机制的Demo几乎差不多,主要差别就是在Activity中我们重写了Activity的dispatchTouchEvent与onTouchEvent方法。
我们看一下运行效果,点击Button,打印信息如下:
1 MainActivity__dispatchTouchEvent__action:0
2 MyLinearLayout_dispatchTouchEvent_Action:0
3 MyButton_dispatchTouchEvent_Action:0
4 onTouch___v:com.wl.activitydispatchtouchevent.MyButton___action:0
5 MyButton_onTouchEvent_Action:0
6 MainActivity__dispatchTouchEvent__action:1
7 MyLinearLayout_dispatchTouchEvent_Action:1
8 MyButton_dispatchTouchEvent_Action:1
9 onTouch___v:com.wl.activitydispatchtouchevent.MyButton___action:1
10 MyButton_onTouchEvent_Action:1
11 onClick___v:com.wl.activitydispatchtouchevent.MyButton
除去与Activity有关的信息,其余信息打印顺序相信你应该轻松理解了。我们看到触摸事件实现传递到Activity中的,其次才传递到MyLinearLayout,最后传递给MyButton。是不是触摸事件就是Activity先获取到接下来才继续向下传递的呢?别急着下结论,我们看看Activity中dispatchTouchEvent都做了什么。
Activity事件传递机制源码分析(源码版本为API23)
Activity中dispatchTouchEvent方法源码如下:
1 public boolean dispatchTouchEvent(MotionEvent ev) { 2 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 3 onUserInteraction(); 4 } 5 if (getWindow().superDispatchTouchEvent(ev)) { 6 return true; 7 } 8 return onTouchEvent(ev); 9 }
是不是爽歪歪?这么短,我们看2-4行代码,首先判断如果是ACTION_DOWN事件则执行onUserInteraction()方法,对于onUserInteraction()方法这里不做具体分析,不是本篇重点。
我们继续向下分析,5-9代码,如果if条件成立则直接返回true,不成立则dispatchTouchEvent最终返回值由onTouchEvent决定,那么if判断就是关键了。
5行代码,getWindow()返回mWindow对象,在Activity的attach方法中进行的初始化,如下:
1 final void attach(Context context, ActivityThread aThread, 2 Instrumentation instr, IBinder token, int ident, 3 Application application, Intent intent, ActivityInfo info, 4 CharSequence title, Activity parent, String id, 5 NonConfigurationInstances lastNonConfigurationInstances, 6 Configuration config, String referrer, IVoiceInteractor voiceInteractor) { 7
8 ........... 9 mWindow = new PhoneWindow(this); 10 mWindow.setCallback(this); 11 ........... 12 }
mWindow其实就是PhoneWindow对象,接下来我们找到PhoneWindow类(源码目录:...\sdk\sources\android-23\com\android\internal\policy\)。
PhoneWindow类继承自Window类,我们先看看父类中superDispatchTouchEvent是怎么处理的。
Window类中superDispatchTouchEvent源码如下:
1 /**
2 * Used by custom windows, such as Dialog, to pass the touch screen event 3 * further down the view hierarchy. Application developers should 4 * not need to implement or call this. 5 * 6 */
7 public abstract boolean superDispatchTouchEvent(MotionEvent event);
看到了吧,很简单,父类中就是一个抽象方法, 看注释就知道此方法主要用来屏幕事件传递的,开发者不需要实现或者调用这个方法。
接下来我们看看PhoneWindow类中的superDispatchTouchEvent方法:
1 @Override 2 public boolean superDispatchTouchEvent(MotionEvent event) { 3 return mDecor.superDispatchTouchEvent(event); 4 }
是不是更简单?直接调用mDecor的superDispatchTouchEvent方法,mDecor是什么玩意呢?这里就直说说了,mDecor是DecorView的实例。
DecorView类是PhoneWindow类的内部类,源码如下:
1 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { 2
3 .......... 4 public boolean superDispatchTouchEvent(MotionEvent event) { 5 return super.dispatchTouchEvent(event); 6 } 7
8 .......... 9 }
我勒个去,搞半天DecorView 继承自FrameLayout,我们知道 FrameLayout继承自ViewGroup,最终就是调用ViewGroup中的dispatchTouchEvent方法进行事件分发。
但是到这里我们还有一个疑问,以我们Demo为例,通过上述分析事件先传递到Activity的dispatchTouchEvent方法,然后调用DecorView 的superDispatchTouchEvent方法最终调用的ViewGroup的dispatchTouchEvent方法,但是跟我们Demo中的MyLinearLayout有什么关系呢?或者说是怎么传递到MyLinearLayout的呢?
要解答这个疑问我们就必须熟知我们平时调用Activity中的setContentView方法设置布局的时候我们自己的布局到底是怎么挂载到Activity上的,这篇我们就不进入深入源码解析了,不是本篇重点,直说一些结论性东西。后续会单独写一篇文章专门分析setContentView究竟都做了什么。
我们在调用setContentView设置布局的时候其实都是被放置在id为content的FrameLayout 布局中的,注意id为content的FrameLayout 布局并不是上面讲的DecorView,具体层级关系如下:
看到了吧,id为content的FrameLayout 布局是DecorView的子View布局。我们自己的布局最后总会替换掉id为content的FrameLayout 。
到这里你该明白了吧,Activity将触摸事件经过层层传递给DecorView, 而DecorView会调用ViewGroup的dispatchTouchEvent方法将事件传递给子View。之后的逻辑就是我们上两篇所讲的内容了。
接下来我们回看Activity中dispatchTouchEvent方法,第5行根据我们上述分析的,如果最终找到子View消耗事件则返回值为true,进而整个方法返回true。如果没有子View处理当前触摸事件则返回false,执行Activity中onTouchEvent方法。
我们接下来分析一下Activity中onTouchEvent方法,源码如下;
1 /**
2 * Called when a touch screen event was not handled by any of the views 3 * under it. This is most useful to process touch events that happen 4 * outside of your window bounds, where there is no view to receive it. 5 * 6 * @param event The touch screen event being processed. 7 * 8 * @return Return true if you have consumed the event, false if you haven't. 9 * The default implementation always returns false. 10 */
11 public boolean onTouchEvent(MotionEvent event) { 12 if (mWindow.shouldCloseOnTouch(this, event)) { 13 finish(); 14 return true; 15 } 16
17 return false; 18 }
逻辑也不复杂,主要就是第12行代码,调用mWindow的shouldCloseOnTouch方法,如果此方法返回true则整个方法返回true,反之则返回false。
mWindow上面我们分析过就是PhoneWindow的实例,好了我们就去PhoneWindow类中找shouldCloseOnTouch方法看一下吧,然而PhoneWindow中并没有这个方法,那我们看看父类Window中有没有这个方法呢,果然这个方法在其父类中找到,源码如下:
1 public boolean shouldCloseOnTouch(Context context, MotionEvent event) { 2 if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN 3 && isOutOfBounds(context, event) && peekDecorView() != null) { 4 return true; 5 } 6 return false; 7 }
这里主要逻辑也是一个判断,判断mCloseOnTouchOutside标记以及是否为ACTION_DOWN事件,然后判断点击事件的坐标x,y有没有超出边界,最后调用peekDecorView()判断是否为空。peekDecorView()在Window类中就是一个抽象方法,具体实现在PhoneWindow类中如下:
1 public final View peekDecorView() { 2 return mDecor; 3 }
很简单,就是返回mDecor,上面我们分析过mDecor就是DecorView的实例,这里我们需要知道我们在Activity中调用setContentView的时候mDecor就会初始化,具体分析会在下一篇文章中,这里只要知道mDecor不为null就可以了。
其余的都不难理解但是mCloseOnTouchOutside 是个什么鬼呢?我们知道Activity设置成Dialog样式的时候默认点击外部的时候是会关闭的,同样我们也可以调用setFinishOnTouchOutside(false)设置为点击外部时候Activity不关闭,mCloseOnTouchOutside 就是用来记录这个的,如果我们将Activity设置为Dialog样式mCloseOnTouchOutside 默认就被设置为true,我们知道大部分情况下Activity是不会设置为Dialog样式的,所以mCloseOnTouchOutside 默认为false。(关于mCloseOnTouchOutside其实是想从源码角度分析一下的,但是这部分内容实在和传递机制不沾边,就这部分有一个判断,所以就不仔细分析了,在下一篇分析setContentView的时候在提一下吧 )
这里我们稍微总结一下:mCloseOnTouchOutside 默认情况下是false,如果Activity样式设置为Dialog系统默认会将mCloseOnTouchOutside 设置为true,所以Dialog样式的Activity默认情况下点击外部会关闭,如果我们调用setFinishOnTouchOutside(false)或者在styles文件中设置了 <item name="android:windowCloseOnTouchOutside">false</item> 那么最终都会将mCloseOnTouchOutside 变量置为false,点击Activity外部也就不会关闭了。
综上分析,Window中shouldCloseOnTouch大多数情况下是返回false的,从而Activity中onTouchEvent大多说情况下也是返回false,除非我们进行了特殊设置。这也就是Activity中onTouchEvent注释是The default implementation always returns false而不是The default implementation returns false,就多了一个always。
好了,到此为止关于安卓事件传递机制最重要的部分都已经讲解完毕,最最核心的还是要掌握View以及ViewGroup的部分,至于Activity的传递大体了解一下流程就可以了。
下一篇我们一起探究一下Activity中setContentView方法究竟做了什么事情。反过来你能更好理解本篇中的某些点。