android开发,涉及到自定义view的时机会非常的多,完成一个非常优质的view,view必须和用户又一个非常良好的交互。view的事件处理就是一个很重要的环节。
本文是对view有一定基础的总结。
Android 触摸机制
责任链模式
在理解事件处理机制之前,我们先理解一下,view和viewGroup的异同。
view
view 在官方文档上是这么写的:This class represents the basic building block for user interface components.也就是说这个类代表用户界面组件的基本构建块。所有在ui界面上看到的东西本质上都是view.或者是从View继承过来的。
比如常见的控件,textview,button,都是继承view。
viewGroup
ViewGroup在官方文档上是这么写的:The ViewGroup subclass is the base class for layouts, which are invisible containers that hold other Views (or other ViewGroups) and define their layout properties.简单来说,viewGroup就是布局。而布局我们最常见的也就是linearLayout,RealtiveLayout,FrameLayout等等。
从类结构,层级上来看,view 是包含 textview,viewGroup的。
view是根,其他的都是衍生出来的
从界面的包含关系上来看,viewGroup 是包含 view,viewGroup的
viewGroup是根,其他的都是附加在viewGroup上的
理解之后,我们从操作上知道,当我们的手,去触摸屏幕的时候,界面会随着我们的操作而进行不同的交互。
触摸的时候,就是一个touch事件处理的过程。
一个触摸事件的具体分发流程
当我们的手指点击屏幕的时候,会产生一系列具体的事件。move ,down ,up。 我们的activity 会第一个接受到事件。
调用 dispatchTouchEvent() 事件。返回值有三种 true,false,super。
true,false 表示touch事件在activity消费掉,不下发给view。
super 表示下发给ViewGroup处理。具体流程看下图。
这边先重点看一下,为什么activty的点击事件,传递到了viewGroup层。
我们首先看一下源码(基于sdk-28):
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
很容易看到getWindow,这样子很容易让我们联想到和view有关。那我们看一下getWindow.superDispatchTouchEvent()做了什么。
/**
* 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);
它是window里面的一个抽象方法。传递touch screen event。而我们知道activity里面有一个phoneWindow,(phoneWindow是window的唯一实现类)所以追踪phoneWindow.superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
看到了mDecor,大概我们就知道怎么传递下去的了。
先看下mDecor的定义。
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
就是我们手机屏幕的最外层view。然后它分发给里面的view,最后我们的view就拿到了传递事件。

touch事件
Touch事件分发中只有两个主角:ViewGroup和View。Activity的Touch事件事实上是调用它内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理。
简单的了解一下Android 处理触摸事件主要涉及到几个方法:
onInterceptTouchEvent(),(只有viewGroup有)
dipatchTouchEvent(), onTouchEvent(), onTouch()。
view和viewGroup都有
onInterceptTouchEvent()从字面上看都是拦截用的。 dipatchTouchEvent() 是分发。 onTouchEvent(), onTouch() 是处理触摸的。
通过上面我们知道,view是没有拦截方法的,也就是它没有办法往下传递。所以就没有中断接受的概念。
其实在view中,dispatchTouchEvent()没什么意思。因为,它分发这个事件,其实就是给自己处理。
所以,一般情况下,我们不该在普通View内重写
dispatchTouchEvent方法,因为它并不执行分发逻辑。
当Touch事件到达View时,我们该做的就是是否在
onTouchEvent事件中处理它。
onTouch()和onTouchEvent()的关系
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)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
先看view关于分发的源码,我们知道onTouchLister的优先级高于onTouchEvent事件。所以onTouch()比onTouchEvent()先调用。
onTouch事件消费完成,返回ture的时候,onTouchEvent()就不会调用。
因为Button的performClick是利用onTouchEvent实现,假若onTouchEvent没有被调用到,那么Button的Click事件也无法响应。
onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。
假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。
内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。
触摸事件具体调用顺序
默认情况下,我们的动作都是从最外层传递到最里层的,一般简单的来说,也就是从ViewGroup到view。而实际处理,则是从最里层开始,一层层到外层。也就是view到ViewGroup。
首先会调用最外层的dispatchTouchEvent(),其实这个方法就是分发事件,返回false,表示此层不接受这个动作,返回给activity的onTouchEvent()处理。如果是true,则不传递动作给下面了,也不会传递给actiivty,就到此为止了。如果是viewGroup的话,接下来会调用onInterceptTouchEvent(),此方法用于拦截动作,返回true,拦截成功,false,不拦截。
动作传递到下一层之后,会继续重复上面的动作。直到传递到最里层,也就是上图的叶子节点。这个时候,会调用onTouchEvent事件。如果返回false,表示继续往上传递,会调用上层的onTouchEvent()的事件,重复一直到执行到最上层。也就是上图的根节点。
在这里有一件事情要注意,也就是,只有当viewGroup里面的所有子控件返回的都是false,才会执行viewGroup的onTouchEvent()。
在目前的情况看来, 似乎只要我们把所有的onTouchEvent都返回false,就能保证所有的子控件都响应本次Touch事件了。但必须要说明的是,这里的Touch 事件,只限于Acition_Down事件,即触摸按下事件,而Aciton_UP和Action_MOVE却不会执行。事实上,一次完整的Touch事 件,应该是由一个Down、一个Up和若干个Move组成的。Down方式通过dispatchTouchEvent分发,分发的目的是为了找到真正需要 处理完整Touch请求的View。当某个View或者ViewGroup的onTouchEvent事件返回true时,便表示它是真正要处理这次请求 的View,之后的Aciton_UP和Action_MOVE将由它处理。当所有子View的onTouchEvent都返回false时,这次的 Touch请求就由根ViewGroup,即Activity自己处理了。
总结一下
Touch 事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、 dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、 onTouchEvent两个相关事件。其中ViewGroup又继承于View。
ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。
触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。
当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。
当某个子 View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行 处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所 在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在 ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从 ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。
当 ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用 super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况 下,触发Acitivity的onTouchEvent方法。
onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。
adroid艺术探索提到的注意点:
- 1)同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
- (2)正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
- (3)某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
- (4)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的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)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
- (10)onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
- (11)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。