本文主要是介绍Android开发艺术笔记 | View的事件分发机制原理详析与源码分析(ing),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
原理解析
- 这里要分析的对象就是MotionEvent,即
点击事件;点击事件的事件分发,本质是对MotionEvent事件的分发过程,
即,
当一个MotionEvent产生了以后,
系统需要把这个事件传递给一个具体的View,
而这个传递的过程就是分发过程。
分发与拦截
点击事件的分发过程由三个重要方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev)
- 用来进行事件的分发传递。
- 如果事件能够传递给当前View,那么此方法一定会被调用,
- 返回值是boolean类型,
返回结果受当前View的onTouchEvent
和下级View的dispatchTouchEvent方法的影响; - 表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
- 在
dispatchTouchEvent()内部调用,用来判断是否拦截某个事件; - 如果当前View
拦截了某个事件,那么在同一个事件序列当中,
此方法不会被再次调用, - 返回结果表示
是否拦截当前事件。
- 该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。
- 一旦拦截,
则执行ViewGroup的onTouchEvent,
在ViewGroup中处理事件,而不接着分发给View。- 且只调用一次,所以后面的事件都会交给ViewGroup处理。
public boolean onTouchEvent(MotionEvent event)
同样在
dispatchTouchEvent方法中调用,用来处理点击事件;返回结果表示
是否消耗当前事件,如果
不消耗,则在同一个事件序列中,
当前View无法再次接收到事件。上述三个方法的区别与关系,可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {boolean consume = false;if (onInterceptTouchEvent(ev)) {consume = onTouchEvent(ev);} else {consume = child.dispatchTouchEvent(ev);}return consume;}
- 通过以上伪代码,可以大致了解点击事件在
View层的传递规则:对于一个
根ViewGroup来说,
点击事件产生后,首先会传递给它,
这时其dispatchTouchEvent会被调用;如果这个ViewGroup的
onInterceptTouchEvent方法
返回true就表示它要拦截当前事件,
接着事件就会交给这个ViewGroup处理,
即它的onTouchEvent方法就会被调用;!!!如果这个ViewGroup的
onInterceptTouchEvent方法
返回false就表示它不拦截当前事件,
这时当前事件就会继续传递给它的子元素,
接着子元素的dispatchTouchEvent方法就会被调用,
如此反复直到事件被最终处理。
- 即,
接收到事件 --> 分发 --> 是否拦截
--> 拦截则就地处理【ViewGroup/View:调用自身onTouch()-->onTouchEvent()-->performClick()-->onClick()】!!!,
否则继续往下传!
这里可以看一下文末的两篇博客!
![]()
事件处理
当一个
View需要处理事件时,
如果它设置了OnTouchListener,
则OnTouchListener中的onTouch方法会被回调;-
这时事件如何处理还要看
onTouch的返回值,如果返回
false,【事件不消费,继续往下传递】
则当前View的onTouchEvent方法会被调用,
接着是performClick()-->onClick()被调用;
然后
它的父容器的onTouchEvent将会被调用,
依此类推。
【注意这里跟onInterceptTouchEvent不一样,onInterceptTouchEvent仅在ViewGroup级,
true表拦截处理,调用ViewGroup自身的onTouch()-->onTouchEvent(),onTouch在View级时候,false表继续流程,调用View自身的onTouchEvent()】如果返回
true,【事件被消费】
那么onTouchEvent方法将不会被调用。
由此可见,
给View设置的OnTouchListener,其优先级比onTouchEvent要高。
在onTouchEvent方法中,
如果当前设置的有OnClickListener,那么它的onClick方法会被调用。
而常用的OnClickListener,其优先级最低,即处于事件传递的尾端。
优先级:
onTouch()-->onTouchEvent()-->performClick()-->onClick()
以上是事件处理方法的优先级顺序,按照这个顺序,
只要排在前面的事件方法返回true,消耗处理了点击事件了,点击事件便就地结束,不再下发,排在后面的点击事件也就不会再被调用和响应了;
【文末有实例】
另,onTouch()的实现需要实现onTouchListener;onTouchEvent()/performClick()直接在自定义View文件中重写即可;onClick()的实现需要实现onClick;
-
当一个点击事件产生后,
其传递过程顺序:Activity -> Window -> 顶级View(上述说的表示View层中的顺序);
顶级View接收到事件后,就会按照事件分发机制去分发事件。
如果一个View的
onTouchEvent返回false,
那么它的父容器的onTouchEvent将会被调用,
依此类推。
【除非下往上回传到某个返回true的onTouchEvent(),
则在那里停止,否则——】如果所有的元素都不处理这个事件,
那么这个事件将会最终传递给Activity处理,
即Activity的onTouchEvent方法会被调用。
- 形象地举个例子,
假如点击事件是一个难题,
这个难题最终被上级领导分给了一个程序员去处理(类似事件分发过程),
结果这个程序员搞不定(onTouchEvent返回了false),
但难题必须要解决,
那只能交给水平更高的上级解决(上级的onTouchEvent被调用),
如果上级再搞不定,那只能交给上级的上级去解决,
就这样将难题一层层地向上抛。
【即一个从上到下(分发传递),再从下到上的过程(onTouchEvent(),
例见事件拦截机制大概流程(Android群英传)中的图例】
关于事件传递机制的一些结论(每一个点前面的短语是一个笔者自提的概况中心,便于记忆)
根据它们可以更好地理解整个传递机制:
(1)【事件序列,定义】
“同一个事件序列” 的定义:
指从手指接触屏幕的那一刻起,
到手指离开屏幕的那一刻结束,
在这个过程中所产生的一系列事件,
这个事件序列以down事件开始,
中间含有数量不定的move事件,
最终以up事件结束。![]()
(2)【处理事件,独一无二】
正常情况下,一个事件序列只能被一个View拦截且消耗!!!
这一条的原因可以参考(3),
因为一旦一个元素拦截了某此事件,
那么同一个事件序列内的所有事件都会直接交给它处理!!!
因此同一个事件序列中的事件不能分别由两个View同时处理!!!
除非,
将本该由某个View自己处理的事件
通过onTouchEvent强行传递给其他View处理。
(3)【事件序列,从一而终】
某个View一旦决定拦截,则这一个事件序列都只能由它来处理
(如果事件序列能够传递给它的话),
并且它的onInterceptTouchEvent不会再被调用!!!
当一个View决定拦截一个事件后,
那么系统会把同一个事件序列内的其他方法都直接交给它来处理,
因此
就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
(4)【短期失信】
某个View一旦开始处理事件,
如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),
那么同一事件序列中的其他事件都不会再交给它来处理,
【即,View放弃处理ACTION_DOWN,便放弃了整个事件序列!!!】
并且事件将重新交由它的父元素去处理,
即父元素的onTouchEvent会被调用。【事件向上“回传”】
即,
事件一旦交给一个View处理,那么它就必须消耗掉!!!
否则同一事件序列中剩下的事件就不再交给它来处理了!!!
好比上级交给程序员一件事,如果这件事没有处理好,
短期内上级就不敢再把事情交给这个程序员做。
(5)【余粮上缴】
如果View不消耗除ACTION_DOWN以外的其他事件,
那么这个点击事件会消失,
此时父元素的onTouchEvent并不会被调用,
并且当前View可以持续收到后续的事件,
最终这些消失的点击事件会传递给Activity处理。
(6)ViewGroup默认不拦截任何事件。
Android源码中
ViewGroup的onInterceptTouch-Event方法默认返回false。
(7)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
(8)View的onTouchEvent默认都会消耗事件(返回true)!!!!!!!
除非它是不可点击的(clickable和longClickable同时为false)。
View的longClickable属性默认都为false,clickable属性要分情况,
比如Button的clickable属性默认为true,
而TextView的clickable属性默认为false。
(9)【enable无用,clickable居上】
View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的!!!!!
只要它的clickable或者longClickable有一个为true,
那么它的onTouchEvent就返回true!!!
(10)onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
(11)【由外而内;以下犯上】
事件传递过程是由外向内的,
即事件总是先传递给父元素,然后再由父元素分发给子View,
通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
稍微复习一下:
事件方法的优先级:onTouch()-->onTouchEvent()-->performClick()-->onClick()
以上是事件处理方法的优先级顺序,按照这个顺序,
只要排在前面的事件方法返回true,消耗处理了点击事件了,点击事件便就地结束,不再下发,排在后面的点击事件也就不会再被调用和响应了;
下面是关于事件优先级的一个实例:
public class DragView3 extends View implements View.OnClickListener {private int lastX;private int lastY;public DragView3(Context context) {super(context);ininView();}public DragView3(Context context, AttributeSet attrs) {super(context, attrs);ininView();}public DragView3(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);ininView();}private void ininView() {setBackgroundColor(Color.BLUE);this.setOnClickListener(this);//测试onTouchEvent与onClick的优先级!!}@Overridepublic boolean onTouchEvent(MotionEvent event) {int x = (int) event.getX();int y = (int) event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:// 记录触摸点坐标lastX = (int) event.getX();lastY = (int) event.getY();break;case MotionEvent.ACTION_MOVE:// 计算偏移量int offsetX = x - lastX;int offsetY = y - lastY;ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();layoutParams.leftMargin = getLeft() + offsetX;layoutParams.topMargin = getTop() + offsetY;setLayoutParams(layoutParams);break;}return true;}//测试onTouchEvent与onClick的优先级!!@Overridepublic void onClick(View v) {setBackgroundColor(Color.RED);}
}
- 如上代码,
给自定义View配置了
onClick监听器,
如果onClick能响应,点击View之后会从蓝色变成红色,
但是运行之后我们发现并没有变色,即onClick没有被调用;
View响应的只是onTouchEvent中的滑动逻辑而已。(下面图一)这是因为
onTouchEvent返回true,把事件消耗掉了!!
于是事件在onTouchEvent中处理结束,不再往下传,传不到onClick那里!!!如果,
将以上代码中的onTouchEvent注释掉,
使之默认返回false,不消耗事件,这时onClick则会响应!
那么再次运行程序,可以发现点击View之后,
View从蓝色变成红色!!!(下面图二)
- 由此,
事件处理方法的优先级不言而喻!
图一
图二
小结
- 三个关键方法:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent;分别的作用和关系;- 分发与拦截,是一个依据
分发顺序的从上往下的过程!!!!!
逻辑骨架就是,
接收到事件 --> 分发 --> 是否拦截
--> 拦截则就地处理【ViewGroup/View:调用自身onTouch()-->onTouchEvent()-->performClick()-->onClick()】!!!,
否则继续往下传,传到最下层的View为止,接着进入处理过程!
分发的顺序是Activity -> Window(PhoneWindow) -> DecorView -> 顶级View(上述说的表示View层中的顺序) -> ViewGroup -> View;
这里可以看一下文末的两篇博客!![]()
- 事件的处理则是分发的“回溯”,!!!!!
顺序与分发相反,是一个从下到上的过程,
从最下层的View开始到最上层(即Activity),
如果所有元素都不消耗这个事件,事件最终就传回Activity;
消耗指onTouch、onTouchEvent、onClick等;
源码分析
- 上面说了,
Android事件分发流程: Activity -> ViewGroup -> View;- 所以,想充分理解Android分发机制,本质上是要理解:
Activity对点击事件的分发过程ViewGroup对点击事件的分发过程View对点击事件的分发过程
Activity对点击事件的分发过程
点击事件用MotionEvent来表示,
当一个点击操作发生时,事件最先传递给当前Activity,
由Activity的dispatchTouchEvent来进行事件派发,
具体的工作是由Activity内部的Window来完成的!!!!!!!!Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器),
通过Activity.getWindow.getDecorView()可以获得。先从Activity的dispatchTouchEvent开始,源码:
public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);}
如上,
首先事件开始交给Activity所附属的Window进行分发,如果返回true,
整个事件循环就结束了:
if (getWindow().superDispatchTouchEvent(ev)) {return true;}
返回false意味着事件没有元素处理,
所有View的onTouchEvent都返回了false,
那么Activity的onTouchEvent就会被调用。
return onTouchEvent(ev);
接下来看Window是如何将事件传递给ViewGroup的;
Window是个抽象类!!!
而Window的superDispatchTouchEvent方法也是个抽象方法!!!
因此我们必须找到Window的实现类才行。源码:public abstract boolean superDispatchTouchEvent(MotionEvent event);Window的实现类其实是
PhoneWindow,
这一点从Window的源码中有这么一段话:
Abstract base class for a top-level window look and behavior policy.
An instance of this class should be used as the top-level view added to
the window manager. It provides standard UI policies such as a background, title area,
default key processing, etc.
The only existing implementation of this abstract class is android. policy.
PhoneWindow,which you should instantiate when needing a Window.
Eventually that class will be refactored and a factory method added for creating
Window instances without knowing about a particular implementation.
-
大概是说,
Window类可以控制顶级View的外观和行为策略!!!- 它的
唯一实现位于android.policy.PhoneWindow中!!! - 当你要
实例化这个Window类的时候,
你并不知道它的细节,因为这个类会被重构,
只有一个工厂方法可以使用。
所以可以看下
android.policy.PhoneWindow,
尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。由于Window的唯一实现是
PhoneWindow,
接下来看PhoneWindow是如何处理点击事件的,PhoneWindow.superDispatchTouchEvent源码:
public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}
可以清楚看到,
PhoneWindow将事件直接传递给了DecorView!!!!!!!!!!DecorView是什么:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker// This is the top-level view of the window,containing the window decor.private DecorView mDecor;@Override
public final View getDecorView() {if (mDecor == null) {installDecor();}return mDecor;}
通过
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)可以获取Activity所设置的View!!!!!!!!
这个mDecor就是getWindow().getDecorView()返回的View!!!
而通过setContentView设置的View是它(DecorView mDecor)的一个子View【所谓顶级View】!!!至此,事件传递到了
DecorView这儿,
由于DecorView继承自FrameLayout且是父View,
所以最终事件会传递给View!!!
从而应用能响应点击事件!!从这里开始,
事件已经传递到顶级View了,
即
在Activity中通过setContentView所设置的View,
另外顶级View也叫根View,顶级View一般都是ViewGroup。
顶级View对点击事件的分发过程
点击事件达到顶级View(一般是一个ViewGroup)以后,
会调用ViewGroup的dispatchTouchEvent方法,
然后,
如果顶级ViewGroup拦截事件即onInterceptTouchEvent返回true,
则事件由ViewGroup处理,
如果ViewGroup的mOnTouchListener被设置则onTouch会被调用,
否则onTouchEvent会被调用。
如果都提供的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用!!!!!!!!!
如果顶级ViewGroup不拦截事件,
则事件会传递给它所在的点击事件链上的子View,
这时子View的dispatchTouchEvent会被调用。
到此,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。
以上是对原理部分的回顾;
下面开始顶级View的源码分析;
- ViewGroup对点击事件的分发过程,
其主要实现在ViewGroup的dispatchTouch-Event方法中,
这个方法比较长,这里分段说明。
首先下面一段,描述当前View是否拦截点击事情这个逻辑。
// Check for interception.final boolean intercepted;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;}
- 如上,
ViewGroup在如下两种情况下会判断是否要拦截当前事件:
事件类型为ACTION_DOWN或者mFirstTouchTarget != null。
ACTION_DOWN事件好理解,那么mFirstTouchTarget != null是什么?这个从后面的代码逻辑可以看出来,
当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素【于是 != null】,
换种方式来说,
当ViewGroup【不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null】。
反过来,
一旦事件由当前ViewGroup拦截时,mFirstTouchTarget != null就不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked == MotionEvent. ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。
当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。在下面的代码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用request-DisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。
...
参考:
- 《Android开发艺术探索》
- 《Android群英传》
- Android事件分发机制详解(源码)!!!
- 事件拦截机制大概流程(Android群英传)
- 要点提炼|开发艺术之View
这篇关于Android开发艺术笔记 | View的事件分发机制原理详析与源码分析(ing)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!