Android手机做为手持设备,界面显示区域并非很大,为了有便携的效果,只能牺牲手机的显示区域;这就会带来一个问题,可视内容少;为了避免影响用户体验,咱们必需要在有限的区域作更多的展现,这就对界面的设计有很高的要求了;假如咱们是Google的工程师,咱们要怎么来设计界面,以此带来好的体验效果呢?html
第一种设计:将界面显示区域切割,根据所须要显示的视图,切割为无数块,每一块对应着一部分视图;以下:android
这种界面设计简单粗暴,须要多少个视图,就将界面切割成多少个视图模块,以此来放下全部的视图内容;固然,这样设计显而易见会有问题,当视图愈来愈多的时候,每个视图的模块所能展现的区域就会愈来愈小,这样体验效果是确定不行的;git
第二种设计:既然经过切割显示区域以此来展现视图的方案有问题,那么咱们就来试试重叠的效果吧;以下:github
这种设计很好的解决了视图模块过多时,显示区域不够展现的问题;可是也会存在问题,每个显示区域和用户的交互顺序混乱了,好比我要和模块为4的视图作交互,结果触发了视图5的交互效果,而脑洞一方案则没有该问题;既然如此,那么咱们能不能针对脑洞二的方案来进行优化呢?
答案是:有的!设计模式
当多个模块视图重叠时,要协调好与用户的交互就极其重要了,毕竟涉及到用户体验;bash
当用户的触碰屏幕的显示区域,咱们并不知道哪一个模块须要和用户进行交互,而咱们又不能让用户和其中一个模块的交互失效,那么咱们只能去遍历重叠的模块,由内部的视图来决定是否须要相应用户的操做;ide
这样就能够解决多个模块视图重叠时,哪一个模块须要相应用户交互的问题了;oop
而这正是Android的事件分发机制;布局
固然上面只是个人脑洞,用于方便理解,若是你有更好的想法,能够和我交流;post
那么这种机制是怎么来实现这种效果的呢?请继续往下看;
在深刻分析事件分发以前,先来了解一下事件的来源;
当屏幕被触摸,Linux内核会将硬件产生的触摸事件包装为Event存到/dev/input/event[x]目录下。
接着,系统建立的一个InputReaderThread线程loop起来让EventHub调用getEvent()不断的从/dev/input/文件夹下读取输入事件。
而后InputReader则从EventHub中得到事件交给InputDispatcher。
而InputDispatcher又会把事件分发到须要的地方,好比ViewRootImpl的WindowInputEventReceiver中。
这里只是简单了解一下大概的流程,源码过于复杂,这里不作具体的分析;
归纳之:当触摸屏幕的时候,硬件会捕捉到用户的触摸动做,告诉系统内核,系统内核将该事件保存下来,而后有一个线程会将这个事件读取出来,交由专门分发的类进行分发;
当屏幕被触摸时,系统底层会将触摸事件(坐标和时间等)封装成MotionEvent事件返回给上层 View;从用户首次触摸屏幕开始,经历手指在屏幕表面的任何移动,直到手指离开屏幕时结束都会产生一系列事件;
MotionEvent的类型:
在分析事件分发机制以前,咱们先来看一下事件分发涉及的涉及模式;
这个设计模式是事件分发机制的核心,Google工程师是经过这个设计模式来设计事件分发机制的;理解了这个设计模式有助于咱们理解事件分发机制;
而这个设计模式就是责任链模式;
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求建立了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。
在这种模式中,一般每一个接收者都包含对另外一个接收者的引用。若是一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
下面咱们经过一段伪代码来解读这个模式:
// 请求
switch (request) {
case 0:
// 对象一接收请求并处理
break;
case 1:
// 对象二接收请求并处理
break;
case 2:
// 对象三接收请求并处理
break;
case 3:
// 对象四接收请求并处理
break;
case 4:
// 对象五接收请求并处理
break;
default:
// 默认对象接收请求并处理
}
复制代码
上面这个就是咱们用的最熟悉的责任链模式,当有一个请求进入责任链的时候,会遍历当前责任链上全部的对象,若是匹配到了则提早结束遍历,若是匹配不到则会被默认的对象接收;
责任链的本质是一个单向的链表结构,当有请求进入时,只会单向传递,直到被接收;
上面咱们理解了责任链设计模式以后,接下来咱们来看看事件分发机制的具体实现;
在上上篇博客里面分析了View的绘制流程,里面提到了View的层次关系,Activity是View的宿主,而最顶层的View是DecorView,而DecorView里面则是View树的结构,那么咱们将这些关系一一对应到了责任链里面,来看看效果吧;
当有一个事件进入责任链时,会从最顶层的DecorView开始往View树传递,直到被其中一个对象所消费;
那么由此可知事件分发总共能够分为三个部分;
接下来先来看一下事件分发机制的核心方法,主要有三个;
下面咱们经过Demo来看看事件是怎么传递的?
写了一个简单的布局,一个RelativeLayout里面放一个按钮;
接下来点击屏幕,看看流程会怎么走;
step1:当点击屏幕的时候,会产出一个ACTION_DOWN的事件,传递到了Activity的dispatchTouchEvent方法里,来看一下Activity的dispatchTouchEvent方法,这里调用了super.dispatchTouchEvent(ev),也就是走了父类的dispatchTouchEvent方法;
这里面有三个方法,第一个onUserInteraction()是空方法;
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activitys {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
复制代码
将注释翻译过来的意思就是:
每当Key,Touch,Trackball事件分发到当前Activity就会被调用。若是你想当你的Activity在运行的时候,可以得知用户正在与你的设备交互,你能够override该方法。
这个回调方法和onUserLeaveHint是为了帮助Activities智能的管理状态栏Notification;特别是为了帮助Activities在恰当的时间取消Notification。
全部Activity的onUserLeaveHint 回调都会伴随着onUserInteraction。这保证当用户相关的的操做都会被通知到,例以下拉下通知栏并点击其中的条目。 这个方法不是重点,不须要过多关注;
须要关注的是第二个方法getWindow().superDispatchTouchEvent(ev),这个方法最终走的是PhoneWindow的superDispatchTouchEvent();
step3:这个mDecor是DecorView,看看DecorView里的superDispatchTouchEvent(ev)方法作了啥?
这里面仍是调的super,走的父类的方法;
最终走的是ViewGroup的dispatchTouchEvent()方法;在这个方法里面经过遍历当前全部的子View,经过子View的dispatchTouchEvent()方法将事件传递下去;ViewGroup的事件分发请看下面的分析;
到这里Acitivity事件就已经传递到ViewGroup了,若是后续的对象都没有处理该事件,即getWindow().superDispatchTouchEvent(ev)方法返回false时,Activity就会经过onTouchEvent()把当前的事件处理掉;
看一下Activity的onTouchEvent()里面作了啥?
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
// Window里面的方法;
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
复制代码
Activity的onTouchEvent()会判断当前的事件是否在屏幕的边缘触发的,若是是,则返回true,不然返回false;
总结为流程图:
接下来咱们来分析一下ViewGroup的事件分发;
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// step1;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
}
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
// step2;
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
...
}
...
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// 将当前的事件分发下去;
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
...
}
复制代码
step1:在ViewGroup的dispatchTouchEvent()方法里面,在进行事件分发以前,会先调用onInterceptTouchEvent(ev)方法,用于判断当前的事件是否拦截,若是被拦截了,则事件不分发给子类了,若是没有拦截则继续分发下去;
这里须要注意的是,当事件为MotionEvent.ACTION_DOWN,才会走进onInterceptTouchEvent(ev)方法;
在走这个onInterceptTouchEvent(ev)方法以前,还有一个判断条件,disallowIntercept,这个条件是用来判断是否要禁用拦截事件,若是禁用了,则不会调用拦截的方法了;子类能够经过调用requestDisallowInterceptTouchEvent()方法修改;
若是ViewGroup的子类若是没有重写onInterceptTouchEvent(ev)这个方法,那么就会走ViewGroup的方法,这里用了4个判断条件,可是默认都是走的false,不拦截事件;
step2:若是事件没有被拦截,那么就会遍历当前全部的子View,而后调用子View的dispatchTouchEvent()方法,将事件分发下去;
那若是被拦截了,则会走super.dispatchTouchEvent(event)方法,也就是View的dispatchTouchEvent(event)方法;这个逻辑写在dispatchTransformedTouchEvent()方法里;
到这里ViewGroup的分发就讲完了,至于ViewGroup拦截事件后,怎么处理事件,请看下面的View事件分析;
流程图:
View的事件分发也是调用的dispatchTouchEvent(event)方法,让咱们来看一下这个方法的逻辑;
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// step1
result = true;
}
if (!result && onTouchEvent(event)) {
// step2
result = true;
}
}
...
return result;
}
复制代码
经过源码发现,当事件分发到了View的dispatchTouchEvent(event)后,事件就不会再继续分发下去了;那么这里面的逻辑是怎样的呢?
step1:先判断当前View的状态是可响应的((mViewFlags & ENABLED_MASK) == ENABLED),再判断触摸监听mOnTouchListener的onTouch()的返回值,若是子类实现了OnTouchListener这个监听,而且返回了true,那么dispatchTouchEvent(event)就会返回true,表示当前View已经处理该事件;
step2:判断当step1的状态为false时,则调用了onTouchEvent(event)来判断子类是否返回true,返回true则表示当前View已经处理该事件;
看一下onTouchEvent(event)的源码:
public boolean onTouchEvent(MotionEvent event) {
// 判断当前状态是不是可点击的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
performClickInternal();
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0, x, y);
break;
...
}
return true;
}
return false;
}
复制代码
这里须要关注的是MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件;
经过判断当前的视图是否处于按压状态,且判断此视图添加的窗口数量是否和原始的一致,若是这两种状态都知足,就会触发长按监听回调;最终调用是在performLongClickInternal()方法里面;
流程图:
到这里,事件分发的流程就已经讲完了;
让咱们来回忆一下上面提到的三个方法:
分析到这里,关于上面脑洞一的设计,这种分发机制是否是完美的解决了交互的问题;
不管你视图重叠多少,事件都会一层层的传递过去,直到被某一层处理掉;有了这个机制,Android的界面就变的更灵活,更有创造性了;
看一下汇总的流程图:
关于自定义View相关的文章,以前也总结了几篇,感兴趣的能够看一下;