版权声明:本文出自汪磊的博客,转载请务必注明出处。
在前两篇我们共同探讨了事件传递机制《View篇》与《ViewGroup篇》,我们知道View触摸事件是ViewGroup传递过去的,比如一个很简单的布局最外层是LinearLayout,里面就一个Button,我们点击Button的时候触摸事件是由外层LinearLayout传递给里面Button的,但是有没有想过当前触摸事件是谁传递给外层的LinearLayout的呢?带着这个疑问我们继续来共同探讨一下。
从Demo示例说起
我们还是先写一个简单的demo,很简单,代码如下:自定义Button:
public class MyButton extends Button { private final String TAG = "WL"; public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//
Log.i(TAG, "MyButton_dispatchTouchEvent_Action:"+ev.getAction());
return super.dispatchTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent event) {
//
Log.i(TAG, "MyButton_onTouchEvent_Action:"+event.getAction());
return super.onTouchEvent(event);
}
}
自定义LinearLayout:
public class MyLinearLayout extends LinearLayout { private final String TAG = "WL"; public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//
Log.i(TAG, "MyLinearLayout_dispatchTouchEvent_Action:"+ev.getAction());
return super.dispatchTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent event) {
//
Log.i(TAG, "MyLinearLayout_onTouchEvent_Action:"+event.getAction());
return super.onTouchEvent(event);
}
}
布局文件:
<com.wl.activitydispatchtouchevent.MyLinearLayout
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"
android:background="#0099cc"
android:id="@+id/mylinearlayout"
android:gravity="center"
tools:context="com.wl.activitydispatchtouchevent.MainActivity" > <com.wl.activitydispatchtouchevent.MyButton
android:id="@+id/mybutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="WL_Button" /> </com.wl.activitydispatchtouchevent.MyLinearLayout>
Activity中代码:
public class MainActivity extends Activity implements OnClickListener,
OnTouchListener { private final String TAG = "WL"; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fullscreen);
//
findViewById(R.id.mybutton).setOnClickListener(this);
findViewById(R.id.mybutton).setOnTouchListener(this);
//
findViewById(R.id.mylinearlayout).setOnClickListener(this);
findViewById(R.id.mylinearlayout).setOnTouchListener(this);
} @Override
public boolean onTouch(View v, MotionEvent event) {
//
Log.i(TAG, "onTouch___v:" + v + "___action:" + event.getAction());
return false;
} @Override
public void onClick(View v) {
//
Log.i(TAG, "onClick___v:" + v);
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "MainActivity__dispatchTouchEvent__action:" + ev.getAction());
return super.dispatchTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "MainActivity___onTouchEvent___action=" + event.getAction());
return super.onTouchEvent(event);
}
}
怎么样,很简单吧。和上一篇讲解ViewGroup传递机制的Demo几乎差不多,主要差别就是在Activity中我们重写了Activity的dispatchTouchEvent与onTouchEvent方法。
我们看一下运行效果,点击Button,打印信息如下:
MainActivity__dispatchTouchEvent__action:0
MyLinearLayout_dispatchTouchEvent_Action:0
MyButton_dispatchTouchEvent_Action:0
onTouch___v:com.wl.activitydispatchtouchevent.MyButton___action:0
MyButton_onTouchEvent_Action:0
MainActivity__dispatchTouchEvent__action:1
MyLinearLayout_dispatchTouchEvent_Action:1
MyButton_dispatchTouchEvent_Action:1
onTouch___v:com.wl.activitydispatchtouchevent.MyButton___action:1
MyButton_onTouchEvent_Action:1
onClick___v:com.wl.activitydispatchtouchevent.MyButton
除去与Activity有关的信息,其余信息打印顺序相信你应该轻松理解了。我们看到触摸事件实现传递到Activity中的,其次才传递到MyLinearLayout,最后传递给MyButton。是不是触摸事件就是Activity先获取到接下来才继续向下传递的呢?别急着下结论,我们看看Activity中dispatchTouchEvent都做了什么。
Activity事件传递机制源码分析(源码版本为API23)
Activity中dispatchTouchEvent方法源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
是不是爽歪歪?这么短,我们看2-4行代码,首先判断如果是ACTION_DOWN事件则执行onUserInteraction()方法,对于onUserInteraction()方法这里不做具体分析,不是本篇重点。
我们继续向下分析,5-9代码,如果if条件成立则直接返回true,不成立则dispatchTouchEvent最终返回值由onTouchEvent决定,那么if判断就是关键了。
5行代码,getWindow()返回mWindow对象,在Activity的attach方法中进行的初始化,如下:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) { ...........
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
...........
}
mWindow其实就是PhoneWindow对象,接下来我们找到PhoneWindow类(源码目录:...\sdk\sources\android-23\com\android\internal\policy\)。
PhoneWindow类继承自Window类,我们先看看父类中superDispatchTouchEvent是怎么处理的。
Window类中superDispatchTouchEvent源码如下:
1 /**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
看到了吧,很简单,父类中就是一个抽象方法, 看注释就知道此方法主要用来屏幕事件传递的,开发者不需要实现或者调用这个方法。
接下来我们看看PhoneWindow类中的superDispatchTouchEvent方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
是不是更简单?直接调用mDecor的superDispatchTouchEvent方法,mDecor是什么玩意呢?这里就直说说了,mDecor是DecorView的实例。
DecorView类是PhoneWindow类的内部类,源码如下:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { ..........
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
} ..........
}
我勒个去,搞半天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方法,源码如下;
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
} return false;
}
逻辑也不复杂,主要就是第12行代码,调用mWindow的shouldCloseOnTouch方法,如果此方法返回true则整个方法返回true,反之则返回false。
mWindow上面我们分析过就是PhoneWindow的实例,好了我们就去PhoneWindow类中找shouldCloseOnTouch方法看一下吧,然而PhoneWindow中并没有这个方法,那我们看看父类Window中有没有这个方法呢,果然这个方法在其父类中找到,源码如下:
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
这里主要逻辑也是一个判断,判断mCloseOnTouchOutside标记以及是否为ACTION_DOWN事件,然后判断点击事件的坐标x,y有没有超出边界,最后调用peekDecorView()判断是否为空。peekDecorView()在Window类中就是一个抽象方法,具体实现在PhoneWindow类中如下:
public final View peekDecorView() {
return mDecor;
}
很简单,就是返回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方法究竟做了什么事情。反过来你能更好理解本篇中的某些点。