View触摸分析

Android用户消息

用户消息是指经过消息处理前端把硬件物理消息转换成Framework内部定义的统一格式后的消息。这些消息目前分为三类,分别是按键消息(KeyEvent)、点消息(Pointer)或者也叫触摸消息、轨迹球
( Trackball)消息。重点关注前两类即可。

按键消息

按键消息的实现类是android.view.KeyEvent,该类定义了消息包含的参数,以及获取这些参数的
API接口。

  • getAction(): 该函数返回按键动作,DOWN 或者 UP
  • getKeyCode():该函数返回按键代码,这些代码是Android内部统一定义的,原始消息必须被转
    换成此代码才能被Framework处理。比如数字0〜9,字 符 A〜Z 等。当然,如果需要硬件的原始消息键值,可调用getScanCode()获得。
  • getRepeat():该函数返回从按下后重复的次数。简单的讲,如果在DOS窗口中,按住字符“A ”
    键不放,那 么 getRepeat()的次数就等于屏幕上显示的A 的个数减1。

以上过程需要注意两点。

第一点,loop次数从1到 2,该过程往往会用比较多的时间,而 从 2到 3、3到 4、4到 5等用的时间却相同,并且少于从1到 2。

第二点,View系统内部所能获得的消息已经被前端处理过了,这种处理包括第一次按键的延时处 理,而 在 View内部还进行了长按监测,因此,实际上从DOWN消息到发生长按,消息处理回调的时间由两部分组成,一个是消息处理前端所指定的第一次延迟,另一个则是View内部定义的一个长按时限。

触摸消息

触摸消息的实现类在android.view.MotionEvent中,该类定义了和触摸相关的消息参数,并提供了
一组API 接口让用户获取这些参数。

  • getAction():获取消息动作,触摸消息动作的定义远远大于按键消息的动作,原因是当前的大多数触摸屏都支持多点触控,因此,触摸消息中必须包含是哪个点按下或者释放。
  • getEventTime()和 getDownTime():前者获取本次消息发生的时间,而后者获取DOWN消息发生的时间,如果本次消息就是DOWN消息,两者的值相同,如果本次不是DOWN消息,那么则为前面最后一次DOWN消息发生的时间。
  • getPressure:获取用户点击力量的大小,其值可以大于1
  • getX(int index)和 getY(int index): 返回指定触摸点对应的坐标,对于多点触控而言,参数index代表哪个点,从 0开始。

按键消息总体派发过程

首先,在 ViewRoot中定义了一个 InputHandler对 象 ,当底层得到按键消息后,会回调到该
InputHandler对象的handleKey()函数,该函数再调用ViewRoot中的dispatchKey()函数,该函数内部发送一个异步DISPATC_KEY消息,消息的处理函数为deliverKeyEvent(),该函数内部分以下三步执行。

  1. 调 用 mView.dispatchKeyEventPreIme(),这里的Prelme的意思就是“在 Ime” 之前,即输入法之前。因为对于View系统来讲,如果有输入法窗口存在,会先将按键消息派发到输入法窗口,只有当输入法窗口没有处理该消息时,才会把消息继续派发到真正的视图。所以,对于有输入法窗口显示的情况,如果应用程序员想在输入法截获消息之前处理该消息,则可以重载
    dispatchKeyEventPreIme(),从而处理一些特定的按键消息。执行完dispatchKeyEventPreIme()后,如果该函数返回为true,则可以直接返回了,但在返回之前如果WmS要求返回一个处理回执,则需要先调用finishlnputEvent()报告给WmS已经处理的该消息,从而使得WmS可以继续派发下一个消息。
  2. 接下来就需要把该消息派发到输入法窗口。当然,此时输入法窗口必须存在,如果不存在的话, 则直接派发到真正的视图。
  3. 调 用deliverKeyEventToViewHierarchy(),将消息派发给真正的视图。该函数内部又可分为四步执行。

(1). 调用checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,并且会
消耗掉该消息,一般情况下该函数总是会返回false。
(2) 调 用 mView.dispatchKeyEvent()将消息派发给根视图。对于应用窗口而言,根视图就是 PhoneWindowDecorView对象;对于非应用窗口,mView就是任何ViewGroup的一个实现,比如状态栏窗口,它仅仅是一个FrameLayout视图。该函数内部则真正处理了按键消息,并回调程序员所实现 的消息处理代码。
(3)如果应用程序中没有处理该消息,则默认会判断该消息是否会引起视图焦点的变化,如果会, 则进行焦点切换。比如,桌面上有好多个图标,当按方向“上下” 键时,会聚焦到上一个或下一个图标, 具体过程如下。

1.根据键值判断是哪个方向,当然,如果不是方向键,则会直接返回。
2.调 用 mView.findFocusO,寻找当前视图mView中的拥有焦点的子视图。一般情况下都会找到相 应的子视图,除非mVeiw中的所有子视图都是不可Focus的。如果找到了,则赋值给临时变量focused并继续往下执行。
3.调 用 focused.focusSearch(dir)寻 找focused视图的下一个视图。参 数 dir代表方向,即往哪个方向上寻找下一个焦点视图。如果找到了,并且下一个视图不是当前焦点视图,则将下一个焦点视图赋值给临时变量V 。
4.既然存在下一个焦点视图,接下来就需要让该视图获取焦点。由于下一个焦点窗口需要知道上
一个焦点区的位置,从而确定下一个焦点窗口内部具体应该聚焦到什么地方,所以需要首先计算出上一个焦点区的坐标。

触摸消息派发过程

  1. 触摸消息是消息获取模块直接派发给应用程序的
  2. 触摸消息在处理时,需要根据触摸坐标计算该消息应该派发给哪个View/ViewGroup
  3. 没有类似于“系统按键” 的 “系统触摸键”,应用程序可完全控制触摸行为
  4. 子视图优先父视图处理消息,即首先是子视图处理该消息,只有当子视图消耗该消息时,父视
    图才有机会处理,这与按键消息的处理完全相反。
触摸消息总体派发过程

当消息获取模块通过pipe将消息传递到客户端,InputQueue中的next()函数内部调用nativePollOnce()函数中会读取该消息。如果有消息,则回调ViewRoot内部的mlnputHandler对象的 dispatchMotion()函数,该函数仅仅是发起一个DISPATCH_POINTER异步消息,消息的处理函数是 deliverPointerEvent()。执行完该函数后,调 用 finishInputEvent()向消息获取模块发送一个回执,以便其进行下一次消息派发,真正完成回执的代码是native C++编写的。下面就来介绍deliverPointerEvent()的具体过程:

  1. 进行物理像素到逻辑像素的转换。在一般情况下,物理屏幕的像素等于操作系统中定义的屏幕
    像素,不需要转换,只有当两者不同时才需要转换。比如对于800X480像素分辨率的屏幕,操作系统却将其定义成480X320像素,触摸消息本对应物理屏幕,所以需要转换到系统逻辑坐标。
  2. 如果是DOWN消息,调 用 ensureTbuchMode(true)函数则进入触摸模式,与之相反的是“非触
    摸模式”,即按键模式。该函数会引起相关视图状态的变化。
  3. 将屏幕坐标转换到视图坐标。触摸消息本身的坐标位置是相对于屏幕左上角,对 于 800X480
    像素的屏幕,视图可以认为是没有边界的,它内部处理消息时所需要的坐标是相对于视图本身的,如图 13-5所示。转换的方法很简单,变 量 mCurScrollY记录了该视图在屏幕坐标中的Y 轴滚动,这里请注意,对于根视图而言,没有X轴的滚动,因为根视图的宽度已经被设置为屏幕本身的宽度。
  4. 调 用 mView.dispatchTouchEvent()将消息派发给根视图,该函数内部会继而将消息派发到整个View树。根视图有两种情况,对 于 Activity包含的窗口,根视图就是PhoneWindow中的DecorView; 对于非应用窗口,根视图只是一个普通的ViewGroup。
  5. 如果以上根视图及其所有子视图都没有消耗该消息,最后处理屏幕边界偏移。屏幕边界偏移在
    程序中用英文edge slop表示,它的作用是当用户正好触摸到屏幕边界时,系统自动对原始消息进行一定的偏移,然后在新的偏移后的位置上寻找是否有匹配的视图,如果有则将消息派发到该视图。为什么要 有 “屏幕偏移” 呢?因为对于触摸屏而言,尤其是电容触摸屏,人类手指尖有一定的大小,当触摸到边界时,力量会被自动吸附到屏幕边界,所以,此处根据上下左右不同的边界对象消息原始位置进行一 定的偏移。
根视图内部消息派发过程

首先来看mView.dispatchTouchEvent()的派发过程。该函数是在ViewRoot中调用的,mView的类型
可能有两种情况,对于应用窗口而言,mView是一个PhoneWindow中的DecorView类型;对于非应用 窗口而言,mView是一般的ViewGroup类型。

在 DecorView中,首先判断是否存在Callback对象,它和按键消息派发时的Callback对象一样,
就 是 Activity类 。如 果 没 有 Callback对象,则 直 接 调 用 DeeprView基 类 ViewGroup中的
dispatchTouchEvent()函数。

在 Activity 中,dispatchTouchEvent()的过程如下。

  1. 如果是ACTION_DOWN消息,则调用onUserInteraction()。这与按键消息一样,给应用程序一
    个机会,以便在消息处理前做点什么,该函数默认什么都不干。
  2. 调用所包含的 Window 对象的 superDispatchTouchEvent()
  3. 如 果 Window类没有消耗该消息,则调用onTouchEvent(),该函数默认也是什么都不干,仅仅
    是给应用程序一个处理消息的机会。

下面接着看Window类中的superDispatchTouchEvent()。此 时 Window类的实现就是PhoneWindow
类 ,该函数继而调用 mDecor的 superDispatchTouchEvent(),而 在 DecorView的该函数中又调用
super.disptchTouchEvent(), 即 ViewGroup 的 dispatchTouchEvent 函数。注意这里的调用过程,一般的消 息处理流程是当上一步没有消耗消息时才执行下一个处理逻辑,而在根视图DecorView中,则是当没有 Callback时才调用ViewGroup的消息处理逻辑,而不是当Callback没有消耗消息时才调用ViewGroup的消息处理逻辑,原因就是Callback本身就会调用到ViewGroup的消息处理逻辑。

ViewGroup内部消息派发过程

ViewGroup内部的处理逻辑也采用递归方式,但与按键处理的递归有所不同。触摸消息处理中首先
会把消息派发给View树中最后一个子视图,如果子视图没有消耗该消息,才递归派发给其父视图,而 在按键消息处理时,递归的过程正好相反。

  1. 将布局坐标转换为视图坐标。这两个坐标的概念如图13-6所示。

视图坐标中,视图大小取决于视图本身包含多少内容,不受物理屏幕大小限制。而布局坐标则是有 限的,它是指父视图给某子视图所分配的布局(layout)大小,超过这个大小的区域将不能显示到父视 图的区域中。转换的方法也很简单,只 需 要 使 用 getX()/getY( )获取布局坐标,然后再加上 mScrollX/mScrollY即可。对于递归调用中第一次调用该函数时,getX()的值实际上就是屏幕上的X 轴坐标,getY()的值就是屏幕上Y 轴的值减去状态栏的高度。
为什么要转换呢?因为接下来要判断该坐标点落到了该ViewGroup中的哪个子视图中,子视图的位置都是相对于该ViewGroup的视图坐标的。

  1. 处理DOWN消息,其作用是判断该视图坐标落到了哪个子视图中。

( 1 ) 首先判断该ViewGroup本身是否被禁止获取TOUCH消息,如果没有禁止,并且回调函数
onlnterceptTouchEvent()中没有消耗该消息,则意味着该消息可以传递给子视图。如果子视图消耗了该 DOWN消息,则直接返回true。
(2) 开始寻找子视图。调 用 child.getHitRect(frame)函数获取该子视图在父视图中的布局坐标,即
该 ViewGroup为 该 child分配的位置是什么,这个位置相对于该child来讲是布局坐标,而相对于该
ViewGroup来讲却是视图坐标,参 数 frame是执行完毕后的位置输出矩形。得到位置后,就可以调用 frame.contain()方法判断该消息位置是否被包含到了该child中,如果包含,并 且 该 child也是一个 ViewGroup,则准备递归调用该child的 dispatchToiichEvent(),在调用之前,首先需要把坐标重新转换 到 child的坐标系中。

以上过程听起来有些绕,为了清晰明了,该过程可表示为如图13-7所示。该图以一个例子来说明
这种坐标转换过程,图中空白区域不计尺寸,黑色边框是一个ViewGroup,其中包含了五个child,每 个方块都是一个child,宽为60,局为20,其中黑点为触摸位置,对应位置上的child也是一个ViewGroup, 内部包含了两个白色方框子View。
View触摸分析_第1张图片

( 3 ) 上步中完成了递归操作前的坐标转换工作,接下来判断该child是否是ViewGroup类。如果是就递归调用到 ViewGroup的 dispatchToucheEvent(), 重新从第一步开始执行;如 果 child不是 ViewGroup,而是一个View, 则意味着递归调用的结束。

  1. 如果是 UP 或者 CANCEL 消息,则清除 mGroupFlags 中的 FLAG_DISALLOW_INTERCEPT 标 识,即允许该ViewGroup截获消息。换句话说,常见的情况就是当用户释放手指,下一次再按下时,该ViewGroup本身可以重新截获消息,而在按下还没有释放期间,ViewGroup本身是不允许截获消息的。
  2. 判 断 target变量是否为空。空代表了所有的子窗口没有消耗该消息,所以该ViewGroup本身需
    要处理该消息。在第二步中,如果匹配到某个child ,并且该child消耗了消息后,会将该child赋值给 父视图中的mMotionTarget变量。在该步中,首先要还原消息的原始位置,因为在第二步中,为了判断子视图是否包含该消息中的位置,对位置进行了从布局坐标到视图坐标的转换,而此时则需要把视图坐标重新转换为布局坐标,因为接下来要调用super.dispatchTouchEvent(),即 View类的该函数。View类中处理该函数时,需要布局坐标,详情参见本章后续小节。转换好之后,直接调用 super.dispatchTouchEvent(), 并返回其执行结果,该函数内部仅仅是回调onTouchEvent(),调用之前先判断 mPrivateFlags中是否包含CANCEL_NEXT_UP_EVENT标识,该标识在一般情况下都不会存在,如果存在,则将消息的action类型改为ACTION_CANCEL
  3. 处 理 target存在,并且变量disallowlntercept为 false,即允许截获,在默认情况下ViewGroup
    都是允许截获消息的,只有当该ViewGroup的子视图调用父视图requestDisallowdlnterceptTouchEvent()函数时,方可禁止父视图再次截获消息,但每次UP消息或者CANCEL消息之后,该 ViewGroup又会 重新截获消息。注意,在本步中,如果不允许截获消息,那么也就不会调用onlnterceptTouchEvent()函 数了,如果允许,并且 onIntereptToucheEvent()消耗了该消息,才执行本步的操作。如果这种情况发生,在代码中将消息的action类型修改为CANCEL,艮P “取消”,然后调用target.dispatchTouchEvent(),从而使得目标视图内部能够据此取消之前可能存在的消息跟踪,比如为了监测长按、特定手势等,执行完 毕后返回true。
  4. 在大多数情况下都会执行到该步,即 target存在,并且ViewGmup本身不允许截获消息或者允许截获但是却没有消耗消息,于是调用target.dispatchTouchEvent()把该消息继续交给目标视图处理。在调用该函数前需要检查 target中是否声明过要取消随后的消息,即 mPrivateFlags中包含 CANCEL_NEXT_UP_EVENT,如果是,则把消息 action 值修改为 CANCEL,置空 mMotionTarget 变量, 因为target不想处理接下来的消息了,那么就可以认为没有target 了。

以上就是 Touch消 息 在 ViewGroup内部的递归和派发,分析以上过程时注意区分
onInterceptTouchEvent()onTouchEvent()

onInterceptTouchEvent()是 在 ViewGroup中定义的,即只有ViewGroup的子类能够重载该方法。而
onTouchEvent()函数有两个定义,一个是在View类中定义的,所有View类的子类都可以重载该方法, 包括ViewGmup,另一个是在Activity中定义的,用 户 Activity可以重载该函数。View系统的消息处理 机制中,会先执行视图内部的onTouchEvent,如果没有处理,才会调用Activity中的onTouchEvent()。另外,对 于 ViewGroup而言,在一般情况下会先调用onInterceptTouchEvent(),只有当该函数没有 消耗掉消息,并且其包含的子视图也没有消耗掉该消息时,才会执行该ViewGroup的 onTouchEvent()。 而 对 于 View而言,没 有 onInterceptTouchEvent()被调用。但并不是所有的消息处理过程都是先调用onInterceptTouchEvent(), 只有以下两种情况才会调用到onlnterceptTcmchEvent()

  • 即在以上第2 步骤中,当是DOWN消息,并且ViewGroup允许截获消息时。
  • 即在以上第5 步骤中,当 ViewGroup中存在target对象,并且允许截获消息时。
各种消息监测的基本实现方法

本来本节应该介绍消息从ViewGroup最终派发到View后,View内部的处理流程,但是,在 View内部处理中使用了一种跟踪监测机制,其作用是能够产生长按等不同的消息,因此,本节首先来介绍这种监测的实现方法,然后在下节中具体介绍View内部的消息派发流程。

在触摸消息中,已经实现的监测包括三种,一种叫做pre-pressed,另一种是pressed,最后一种是
长按,三者是按时间划分的,如 图 13-8所示。
View触摸分析_第2张图片
实 现 监 测 的 基本原理是利用 Handler发送一个异步延迟消息,在 如 图 13-8所示中,当发生 ACTION_DOWN消息时,首先发送一个延迟为t0的异步消息,如果在t0时间内,用户释放了屏幕, 即 ACTION_UP消息在t0时间段中产生,则本次触摸过程对应的是pre-pressed处理代码,其语义是“用户 轻 触 (tap) 了一下”。否则,系统会启动长按监测。如果用户在t 1 时间段内释放屏幕,那么系统认为本次操作是一个“press” 操作,否则超过t0时间后释放屏幕就认为是一个长按消息,超过t0时间后, 系统没有再进行监测。

以上监测过程是在View类 的 onTouchEvent()实现的,如果应用程序重载了该函数,并且没有调用
super.onTouchEvent(),那么以上三种消息的回调就不会得到执行。换句话说,如果重载了 onTouchEvent( )
那么该View对象的onLongClick()回调将不被执行。

View内默认消息派发过程

本节分析触摸消息在View类内部的执行过程。

  1. 调用onFilterTouchEventForSecurity()处理窗口处于模糊显示状态下的消息。所谓的模糊显示是指,应用程序可以设置当前窗口为模糊状态,此时窗口内部的所有视图将显示为模糊效果。这样做的目的是为了隐藏窗口中的内容,对于其中各个视图而言,可以设置该视图的HLTER_TOUCHES_WHEN_OBSCURED标识,如存在该标识,则意味着用户希望不要处理该消息。
  2. 回调视图监听者的onTouchO函数,如果监听者消耗了该消息,则直接返回。
  3. 调用onTouchEvent(),应用程序可以重载该函数,但如果没有重载的话,该函数内部有默认的执行方式。默认的执行流程如下。

(1)判断该视图是否为disable状态,如果是,什么都不干,返回true,即消耗该消息。
(2)处理消息代理TouchDelegate。所谓的消息代理是指,可以给某个View指定一个消息处理代理,当 View收到消息时,首先将该消息派发给其代理进行处理。如果代理内部消耗了该消息,则 View不需要再进行任何处理;如果代理没有处理,则 View继续按照默认的逻辑进行处理。源码中该类的注释中说,该类的目的是为了扩大点击区,意思很简单,举例如图13-9所示。
View触摸分析_第3张图片
黑实线框代表了视图本身在屏幕中的大小,在一般情况下,只有当用户点击到该区域时,该 View对象才能处理Touch消息。然而有时却希望实际的点击区域能够大于View本身的区域,如虚线区所示, 这种情况一般发生在该View本身周围没有其他视图时,为了提高点击的准确率故意设置。这个想法是好的,但是源码中却没有真正实现这个目的,因为要实现这个目的,必须在将消息匹配到对应窗口的判断中使用代理视图的大小,而不能使用视图本身的大小,否则,该视图将因为点击位置没有落到视图区而被忽略。然而源码中却没有经过这个判断,所 以 TouchDelegate形同虚设。

( 3 ) 判断该视图是否是可以点击的,如果不可点击,则直接返回false,即不处理该消息。否则,真正开始执行触摸消息的默认处理逻辑,该逻辑中分别处理了 ACTION4DOWN、MOVE和 UP消息, 具体过程如下。

① 在 ACTION_DOWN 消息中,给 mPrivateFlags变 量 添 加 PRESSED标识,并将变量 mHasPerformLongPress置 为 false,然 后 启 动 ta p 监测,即发送一个异步延迟消息,延迟时间为 ViewConfigration.getTapTimeout()

②对于触摸消息而言,消息本身没有repeat的属性,这与按键消息有所不同,一次触摸消息只有一个DOWN消息,接下来就是连续的MOVE消息,并最终以UP消息结束。因此,本步骤中对MOVE消息进行处理。具体逻辑包括,判断是否移动到了视图区以外,如果是,贝_ 除 tap或 者 longPress的监测,并清除mPrivateFlags中的PRESSED标识,然后调用refreshDrawableState()刷新视图的状态,这将导致对背景根据状态进行重绘。

这个处理逻辑有点问题。在一般的GUI系统中,比如HTC magic手机,或者苹果系统,或 者 Windows系统,当用户按下某个按钮时,如果移动光标到按钮外,此时按钮会从高亮恢复到普通状态,但如果用户把光标再移回到该按钮上时,按钮又会重新变为高亮。然而在目前的Android系统中,代码的设计是,当用户把光标再次移回到视图区时,视图不会重新获得聚焦,实际测试结果也是如此。

③处理ACTION_UP消息,代码中判断该UP消息是发生在前面所讲的哪一个监测时间段中,并据此进行不同的处理。

a .查看是否发生在pre-pressed时间段内,如果是,则给变量prepressed赋值为true。
b .无论是发生在pre-pressed区还是发生在press区,都应该让该视图获取焦点,前提是该视图在Touch模式下可以拥有焦点。
c .如果发生在press之后,即长按,则什么都不做,因为长按的异步消息处理中已经处理了长按消息。如果是发生在长按之前,则 变 量 mHasPerformedLongPress为 false,此时调用removeLongPressCallbackO移除还没有执行的长按监测消息。
d .判断变量focusTaken的局部变量是否为false,在一般情况下,该变量都是false, 因为一般情况下视图默认在Touch模式下是不能获得焦点的,所以 requestFocus()会返回false。当然,只有当返回false的情况下,该 UP消息才能导致回调performClick()函数,否则,当 focusTaken为 true时,用户点击某个视图,该视图仅仅是获得焦点,必须再点击一次才能执行performClick()函数。因为再点击一次时, 该视图已经获得了焦点,从而不会调用requestFocus(),所以变量focusTaken为初始值false。该步说明了 tap和 press动作的相同及区别,相同的地方在于它们都会引起视图聚焦或者执行performClick(),区别在于tap不会改变视图的状态,而 press会把视图的状态改变为PRESSED。从功能的角度来看,两者没有本质区别,仅仅是反映的UI效果有少许差别而已。

④ 处 理 ACTION—CANCEL消息,这里只需要清除PRESSED标识,刷新视图状态,然后再关闭
tap监测即可。
( 4) 分别处理tap和 press动作。如果是press动作,则清除PRESSED标识,并改变视图状态;
如果是tap,仅仅发送一个UnSetPressedState异步消息。
(5) 调用 removeTapCallback(),关闭tap监测。

至此,Touch消息的一次处理过程就结束了。

导致View树重新遍历的时机

遍 历 View树意味着整个View需要重新对其包含的子视图分配大小并重绘。在一般情况下,导致
重新遍历的原因主要有三个。

  • 视图本身内部状态变化引起重绘;比如显示属性由GONE到VISIBLE;
  • View树内部添加或者删除了View;
  • View本身的大小及可见性发生变化;比如TextView中的文本内容变多变少了;

这三种情况最后都会直接或间接调用到View中的三个函数:

  • requestLayout()

导致调用requestLayout()函数的情况包括两种:

  1. setVisibility(),当应用程序改变视图显示属性时,由于显示或者不显示将影响其他同级视图的位置。
  2. 应用程序直接或间接调用到该函数,间接调用是指应用程序调用了View类的其他函数,从而简介调用到requestLayout()。
  • requestFocus()

一般由程序直接调用,简介调用是指当用户按”上/下”、”左/右”键时,相关的处理逻辑会间接的调用到该函数。

  • invalidate()

能导致调用invalidate()函数的包括三种情况:

  1. 调用setVisibility():当应用程序改变视图显示属性时。
  2. setSelected():当改变视图Selected状态时。
  3. setEnable():当改变视图Enable状态时。

由于是View树遍历,所以最后都会执行到最顶级父视图中的ViewRootImpl.scheduleTraversals();在该方法内,系统会发起一个异步消息(老版本中直接通过Handler发,新版本4.1中引入了Choreographer,以及对VSync和三级Buffer支持,让页面显示和操作更流畅,具体可以详见《Android Project Butter分析》),然后在异步消息执行过程中调用performTraversals()完成具体的View树遍历;可以参见下图:

View触摸分析_第4张图片

状态的分类

在View视图中定义了多种和界面效果相关的状态。不同的状态一般会显示不同的界面效果,有多种操作会引起这些状态的改变。Android中应用程序是按照消息机制执行的,每次处理一个消息,如果该消息引起状态改变,则代码中仅仅做一些状态标识,然后发送一个异步消息,而不是立即重绘。然后在下一次消息处理中,根据保存的状态数据,绘制不同的界面效果。

selected和focused的区别
  1. 一个窗口中只能有一个视图获得焦点,当用户按键时,获取焦点的视图会变得高亮起来,而一个窗口可以有多个视图处于selected状态。
  2. 按键消息最终会传递到focused视图中,而不是selected视图中。
  3. 当某个视图处于pressed状态时,如果将其selected状态设为false,那么该视图的pressed状态就会被清空。
  4. focused状态一般是由按键操作引起的,pressed状态是由触摸消息引起的,selected则完全是由应用程序主动调用 setSlected()进行控制。
  5. 当视图重绘时,会根据当前不同的状态选择不同的背景图(selector来定义背景时)。
View.refreshDrawableState()

该函数的作用是根据标识,为视图赋予不同的Drawable对象。

 public void refreshDrawableState() {
 
        //该mPrivateFlags添加PFLAG_DRAWABLE_STATE_DIRTY标识,该标识仅在后面调用的drawableStateChanged函数中用于判断是否发生状态变化。
        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
 
        //调用drawableStateChanged(),该函数是一个protected类型,可重载。ViewGroup中重载了该函数的作用仅仅是为了配合FLAG_ADD_STATES_FROM_CHILDREN标识。
        drawableStateChanged();
 
        //如果该视图有父视图,则调用父视图的childdrawableStateChanged。父视图要么是一个ViewGroup类,要么是一个ViewRoot类。
        ViewParent parent = mParent;
 
        if (parent != null) {
            parent.childDrawableStateChanged(this);
        }
    }

    // 对于ViewGroup,如果该ViewGroup中的mGroupFlags中包含FLAG_ADD_STATES_FROM_CHILDREN标识,则意味着该ViewGroup的背景图
    // 也可以随着其子视图的变化而变化,于是调用refreshDrawableState()刷新该ViewGroup自身的背景图。

	// 应用程序可以调用ViewGroup的 setAddStatesFromChildre()函数将该ViewGroup的背景设置为和子视图的背景图同步,即当ViewGroup对象调
	// 用refreshDrawableState函数时,会调用自身的onCreateDrawableState()。该函数中会首先判断是否设置了
	// FLAG_ADD_STATES_FROM_CHILDREN。如果没有该标识,则ViewGroup就直接调用View类的refreshDrawableState()方法,如果存在该标识,则把所有的子视图的标识(pressd、forcus、…)合并成一个,并作为该ViewGroup的背景int[]型数组。
	// 该设计的目的就是为了使ViewGroup的背景状态和子视图的背景状态同步。
	protected void drawableStateChanged() {
        Drawable d = mBackground;
        if (d != null && d.isStateful()) {
 
            //d变量是该视图的背景图.setState内部会根据int[]为d找到真正的Drawable对象。
            d.setState(getDrawableState());
        }
    }
 
 	public final int[] getDrawableState() {
        if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
            return mDrawableState;
        } else {
            //调用onCreateDrawableState()将这些状态转换为一个int[],这个数组的内部格式是预先定义好的,DrawableStateList类可以识别该int[]。
            mDrawableState = onCreateDrawableState(0);
            mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
            return mDrawableState;
        }
    } 
View.onFocusedChanged()

该函数的作用是处理一些由于焦点变化所导致的其他状态变化的逻辑。

protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect){
 
        //判断是获得了焦点还是失去了焦点。
        if (gainFocus) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        }
 
        InputMethodManager imm = InputMethodManager.peekInstance();
        if (!gainFocus) {
            if (isPressed()) {
                //如果没有获取焦点但是当前正处于PRESSED状态,就必须清除PRESSED状态。
                setPressed(false);
            }
            if (imm != null && mAttachInfo != null && mAttachInfo.mHasWindowFocus) {
                //如果正在和输入法进行交互,则调用imm.focusOut(),该函数内部会隐藏输入法窗口,并断开输入法服务和当前窗口的连接。这里要注意的是,该步中是针对当前窗口正在和输入法交互,因为代码中是查询变量 mAttachInfo.mHasWindowFocus。mAttachInfo是View的内部类。mHasWindowFocus表示当前窗口是否是"前台窗口"。
                imm.focusOut(this);
            }
 
            //调用onFocusLost(),见下:
            onFocusLost();
        } else if (imm != null && mAttachInfo != null && mAttachInfo.mHasWindowFocus) {
 
            //如果视图是获得焦点,调用imm.focusIn()。该函数中,如果该视图是可编辑的,将会启动输入法,并显示输入法窗口。
            imm.focusIn(this);
        }
 
        //调用invalidate(),因为在一般情况下焦点改变后,都会引起视图背景图的改变。
        invalidate();
 
        //回调mOnFocusChangeListener().onFocusChange(),应用程序可以实现该回调接口,以便进行其他操作。
        if (mOnFocusChangeListener != null) {
            mOnFocusChangeListener.onFocusChange(this, gainFocus);
        }
 
        if (mAttachInfo != null) {
            mAttachInfo.mKeyDispatchState.reset(this);
        }
    }
protected void onFocusLost() {
        resetPressedState();
    }

 private void resetPressedState() {
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return;
        }
 
        if (isPressed()) {
            //清除Pressed状态
            setPressed(false);
            //删除长按监测
            if (!mHasPerformedLongPress) {
                removeLongPressCallback();
            }
        }
    }
ViewRoot.ensureTouchMode(boolean inTouchMode)

其作用是在触摸模式和按键模式进行切换时对视图的焦点状态进行处理。

boolean ensureTouchMode(boolean inTouchMode) {
        if (DBG) Log.d("touchmode", "ensureTouchMode(" + inTouchMode + "), current "
                + "touch mode is " + mAttachInfo.mInTouchMode);
 
        //判断参数传入的inTouchMode和当前的Touch模式是否相同,如果相同就直接返回false,什么都不做。当前的值存放在View.mAttachInfo.mInTouchMode中。
 
        if (mAttachInfo.mInTouchMode == inTouchMode) return false;
        // tell the window manager
        try {
            //以下都是需要改变,首先报告 WindowManagerService 当前的Touch模式发生变化,因为 WindowManagerService 在进行客户窗口布局时,需要根据客户窗口的Touch模式进行不同的处理。
            sWindowSession.setInTouchMode(inTouchMode);
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
        // handle the change
        //调用ensureTouchModeLocally()函数进行报告
        return ensureTouchModeLocally(inTouchMode);
    }

 private boolean ensureTouchModeLocally(boolean inTouchMode) {
        if (DBG) Log.d("touchmode", "ensureTouchModeLocally(" + inTouchMode + "), current "
                + "touch mode is " + mAttachInfo.mInTouchMode);
 
        if (mAttachInfo.mInTouchMode == inTouchMode) return false;
        //首先给mInTouchMode重新赋值,因为当前状态发生改变。
        mAttachInfo.mInTouchMode = inTouchMode;
        
        //每个View中都包含一个mAttachInfo对象,该对象来源于ViewRoot中的mAttachInfo,该对象内部又有一个mTreeObserver变量,它是一个ViewTreeObserver对象,调用该对象的dispatchOnTouchModeChanged()。
        mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);
 
        //如果函数是要进入Touch状态,则调用enterTouchMode(),否则调用leaveTouchMode()。
        return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
    }
private boolean enterTouchMode() {
        if (mView != null) {
 
            //判断是否拥有焦点,该方法是View类的,ViewGroup中重载了该函数。View.getFocusChild(),表示找到当前View(Group)中包含焦点的子视图,该函数返回当前View直接子视图。
 
            if (mView.hasFocus()) {
                // note: not relying on mFocusedView here because this could
                // be when the window is first being added, and mFocused isn't
                // set yet.

                //找到真正拥有焦点的View或者ViewGroup,这里的mView就是根视图。
                final View focused = mView.findFocus();
 
                //如果存在焦点,并且该视图没有在Touch模式下获取焦点的能力,直接返回false,这种情况下比较少。应用程序可以调用setFocusableInTouchMode()改变该属性,在默认情况下,视图在Touch模式下是不能拥有焦点的。
                if (focused != null && !focused.isFocusableInTouchMode()) {
 
                    //【1】
                    final ViewGroup ancestorToTakeFocus =
                            findAncestorToTakeFocusInTouchMode(focused);
 
                    //如果可以,则直接调用父类视图的.requestFocus(),并将结果直接返回。就是自己没能力问问老爸有没有这个Touch获取焦点的能力。
 
                    if (ancestorToTakeFocus != null) {
                        // there is an ancestor that wants focus after its descendants that
                        // is focusable in touch mode.. give it focus
                        return ancestorToTakeFocus.requestFocus();
                    } else {
 
                        //一般程序都会走到这步,说明可以正常进入Touch并需要清除其焦点(自己没有,但是有可能父视图有)。
                        // nothing appropriate to have focus in touch mode, clear it out
 
                        //该函数将从根上清除所有子视图中的焦点。
                        mView.unFocus();
 
                        //调用ViewTreeObserver的dispatchOnFocusChanged(),以便应用程序对此执行相关的操作,注意~这里是Focus的回调,上面是Touch的回调。
 
                        mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null);
 
                        //置空mFocusedView
                        mFocusedView = null;
 
                        //代表是做了改变了。
                        return true;
                    }
                }
            }
        }
        //表示没变,自己没有这个能力。
        return false;
    }

【1】大多数情况下,Touch模式下不能拥有焦点,因此需要清除焦点。不过,该步骤中首先判断该视图的父视图是否可以在Touch模式下拥有焦点,如果可以的话,则调用父视图的requestFocus(),并返回其结果,如果不可以才开始真正清除焦点。

private boolean leaveTouchMode() {
        if (mView != null) {
 
            //如果该View体系中是有焦点的
 
            if (mView.hasFocus()) {
                // i learned the hard way to not trust mFocusedView :)
 
                //找到真正拥有焦点的View。
 
                mFocusedView = mView.findFocus();
 
                if (!(mFocusedView instanceof ViewGroup)) {
 
                    //如果是View对象,就不用重新给该视图赋予焦点了,直接返回false。
 
                    // some view has focus, let it keep it
                    return false;
                } else if (((ViewGroup)mFocusedView).getDescendantFocusability() !=
                        ViewGroup.FOCUS_AFTER_DESCENDANTS) {
 
                    //如果该视图是一个ViewGroup对象,并且该ViewGroup对象阻止其子视图获得焦点,那么也直接返回false。(ListView就是可以获得焦点的ViewGroup,并且ListView不会直接把焦点传递给其包含的item,而是使用特别的逻辑让item获得焦点。)
 
                    // some view group has focus, and doesn't prefer its children
                    // over itself for focus, so let them keep it.
                    return false;
                }
            }
 
 
            //如果没有找到拥有焦点的视图,则调用ViewRoot.focusSearch()使该视图获得焦点。【2】
            // find the best view to give focus to in this brave new non-touch-mode
            // world
            final View focused = focusSearch(null, View.FOCUS_DOWN);
            if (focused != null) {
                return focused.requestFocus(View.FOCUS_DOWN);
            }
        }
        return false;
    }

【2】参数null代表当前拥有焦点的视图,即当前没有视图拥有焦点。DOWN代表往下继续找,这就是为什么当用户从按键模式切换到Touch模式,然后再进入按键模式时,上一个有用焦点的视图不能再次获得焦点,而是第一个视图获得焦点的原因。对于ListView而言,上一个获得焦点的视图,当重新切换到按键模式后依然能够获得焦点,因为ListView使用了特别的逻辑为所包含的item赋予焦点。

View中超多的属性变量如何管理?

在庞大的View类中会涉及到非常多的状态码,比如是否可用、是否处于按下状态、是否需要重新分配位置、是否需要重绘等等;View树在遍历重绘时会根据不同的变量值来进行相应的操作,为此View中引入了bit标示位来管理这些状态值,分别用mViewFlags和mPrivateFlags变量来管理(随着状态码的增加,在新版本4.2中还有mPrivateFlags2/mPrivateFlags3变量),他们都是int类型的,也就是说理论上每个变量可以用来标示32个状态值,当对各个状态值修改时采用位运算符&|来完成;

其中mViewFlags变量主要用来保存和视图状态相关的值,比如是否可单击、是否可双击、是否可用、是否拥有焦点等;
mPrivateFlags变量主要用来保存和内部逻辑相关的属性,比如是否需要重新分配位置、是否需要重绘、是否刷新View缓存等;
注意:这两个变量之间是有紧密联系的,经常会需要两个变量同时设置某些状态值,可以参见setFlags(…)方法中的具体内容;

requestLayout()

该方法的执行过程很简单,因为当View树进行重新布局时,总是重新给所有的视图都进行布局,而不像重绘是可以指定只绘制某一个小区域的;

从代码层面他只是为mPrivateFlags变量添加FORCE_LAYOUT标识而已;然后逐层请求mParent.requestLayout();详见下图:
View触摸分析_第5张图片

invalidate()

该方法的作用是请求View树重绘;视图及其父视图在界面上是分层先后显示的,父视图位于子视图下面,绘制过程中,首先绘制最底层的根视图,然后绘制其包含的子视图,子视图若是ViewGroup,则继续绘制其子视图,如此迭代至没有子视图为止;

在具体的重绘过程中,一般不会对所有视图都进行重绘,而是只绘制那些“需要绘制”的视图,那如何找出“需要绘制”的区域呢?这就是invalidate方法要完成的功能!

大致的思路是:当View需要重绘时会给mPrivateFlags变量添加DRAWN标识,然后根据所有带该标识的视图边界一起确定最终要重绘的矩形区块,这里面会涉及到不同坐标体系间的换算,可以参见下图:
View触摸分析_第6张图片
代码的具体执行过程是:

  1. View.invalidate()中设置必要的状态位标识之后,会执行到mParent.invalidateChild(…);这里的mParent有两种情况,一种是有父视图ViewGroup,另一种是已经到顶层了为ViewRootImpl;
  2. 若是ViewGroup,会执行完 invalidateChildInParent(…)之后继续调用mParent.invalidateChildInParent(…);
  3. 最终调用到ViewRootImpl.invalidateChildInParent(…),进而执行scheduleTraversals();

    注意:这里会提前判断 mWillDrawSoon局部变量值,若当前已经在执行performTraversals()遍历重绘了,那就不会调用scheduleTraversals(),也就不会发起重绘的异步消息了,但View中设置的各种状态值仍然是有效的,只是会在下次重绘时生效;

scheduleTraversals()

该方法会在多个地方被调用,比如requestLayout()/invalidate()中,而我们又会经常会看到连续调用这两个方法的情况,那这样岂不是会发起两次View树遍历重绘请求?其实是不会的,因为在scheduleTraversals()方法内设置了一个局部变量mTraversalScheduled,若先执行了requestLayout(),那此时mTraversalScheduled为false,发起一个异步消息请求重绘,并将mTraversalScheduled变量值设为true,这样接着调用invalidate()时判断mTraversalScheduled变量值已经不是false了,这样就确保了只发起一个异步重绘请求;参见下图:
View触摸分析_第7张图片

performTraversals()

该方法时系统内进行View树遍历并进行页面重绘的核心方法,内部逻辑还是非常复杂的,约800行代码;老实说偶目前还未完全看懂里面的细节,中间涉及的关联变量实在太多了;但大致的主体流程还是清晰的,就是根据之前设置好的各种状态值,判断是否需要重新计算视图大小(Measure)、是否需要重新分配视图的位置(也叫布局Layout)、以及是否需要重绘视图(Draw),框架过程参见下图,其中每项的具体过程详见后面的具体描述:
View触摸分析_第8张图片

你可能感兴趣的:(自定义View学习,android)