《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)

                 第6章 深入理解控件(ViewRoot)系统

本章主要内容:

·  介绍创建窗口的新的方法以及WindowManager的实现原理

·  探讨ViewRootImpl的工作方式

·  讨论控件树的测量、布局与绘制

·  讨论输入事件在控件树中的派发

·  介绍PhoneWindow的工作原理以及Activity窗口的创建方式

本章涉及的源代码文件名及位置:

·  ContextImpl.java

frameworks/base/core/java/android/app/ContextImpl.java

·  WindowManagerImpl.java

frameworks/base/core/java/android/view/WindowManagerImpl.java

·  WindowManagerGlobal.java

frameworks/base/core/java/android/view/WindowManagerGlobal.java

·  ViewRootImpl.java

frameworks/base/core/java/android/view/ViewRootImpl.java

·  View.java

frameworks/base/core/java/android/view/View.java

·  ViewGroup.java

frameworks/base/core/java/android/view/ViewGroup.java

·  TabWidget.java

frameworks/base/core/java/android/widget/TabWidget.java

·  HardwareRenderer.java

frameworks/base/core/java/android/view/HardwareRenderer.java

·  FocusFinder.java

frameworks/base/core/java/android/view/FocusFinder.java

·  Activity.java

frameworks/base/core/java/android/app/Activity.java

·  PhoneWindow.java

frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java

·  Window.java

frameworks/base/core/java/android/view/Window.java

·  ActivityThread.java

frameworks/base/core/java/android/app/ActivityThread.java

6.1 初识Android的控件系统

第4章和第5章分别介绍了窗口的两个最核心的内容:显示与用户输入,同时也介绍了在Android中显示一个窗口并接受输入事件的最基本的方法。但是这种方法过于基本,不便于使用。直接使用Canvas绘制用户界面以及使用InputEventReceiver处理用户输入是一件非常繁琐恼人的工作,因为你不得不亲历亲为以下复杂的工作:

·  测量各个UI元素(一段文字、一个图片)的显示尺寸与位置。

·  对各个UI元素进行布局计算与绘制。

·  当显示内容需要发生变化时进行重绘。出于效率考虑,你必须保证重绘区域尽可能地小。

·  分析InputEventReceiver所接收的事件的类型,并确定应该由哪个UI元素响应这个事件。

·  需要处理来自WMS的很多与窗口状态相关的回调。

所幸Android的控件系统使得这些事情不需要我们亲历亲为。

自1983年苹果公司发布第一款搭载图形用户界面(GUI)操作系统的个人电脑Lisa以来的三十多年里,图形用户界面已经发展得相当成熟。无论是运行于桌面系统还是Web,每一个面向图形用户界面的开发工具包(SDK)都至少内置实现了用户和开发者所公认的一套UI元素,尽管名称可能有所差异。例如文本框、图片框、列表框、组合框、按钮、单选按钮、多选按钮,等等。Android的控件系统不仅延续了对各种标准UI元素的支持,还针对移动平台的操作特点增加了使用更加方便、种类更加丰富的一系列新型的UI元素。

注意 在Android中,一个UI元素被称为一个视图(View),然而,笔者认为控件才是UI元素的更贴切的名字。因为UI元素不仅仅是为了向用户显示一些内容,更重要的是它们响应用户的输入并进行相应的工作。本书后续部分将以控件来称呼UI元素(View)。

另外,本章的目的并不是介绍如何使用各种Android控件,而是介绍Android控件系统的工作原理。本章要求读者至少应了解使用Android控件的基本知识。

读者所熟知的Activity、各种对话框、弹出菜单、状态栏与导航栏等等都是基于这套控件系统实现的。因此控件系统将是继WMS与IMS两大系统服务之后的又一个需要我们攻克的目标。

6.1.1 另一种创建窗口的方法

在这一小节里将介绍另外一种创建窗口的方法,并以此为切入点来开始对Android控件系统的探讨。

这个例子将会在屏幕中央显示一个按钮,它会浮在所有应用之上,直到用户点击它为止。市面上某些应用的悬浮窗就是如此实现的。

·  首先,读者使用Eclipse建立一个新的Android工程,并新建一个Service。然后在这个Service中增加如下代码:

// 将按钮作为一个窗口添加到WMS中

private void installFloatingWindow() {

    // ① 获取一个WindowManager实例

    finalWindowManager wm =

                    (WindowManager)getSystemService(Context.WINDOW_SERVICE);

 

    // ② 新建一个按钮控件

    finalButton btn = new Button(this.getBaseContext());

   btn.setText("Click me to dismiss!");

 

    // ③ 生成一个WindowManager.LayoutParams,用以描述窗口的类型与位置信息

   LayoutParams lp = createLayoutParams();

 

    // ④ 通过WindowManager.addView()方法将按钮作为一个窗口添加到系统中

   wm.addView(btn, lp);

 

   btn.setOnClickListener(new View.OnClickListener() {

       @Override

       public void onClick(View v) {

            // ⑤当用户点击按钮时,将按钮从系统中删除

           wm.removeView(btn);

           stopSelf();

        }

    });

}

 

    privateLayoutParams createLayoutParams() {

       LayoutParams lp = new WindowManager.LayoutParams();

       lp.type = LayoutParams.TYPE_PHONE;

       lp.gravity = Gravity.CENTER;

       lp.width = LayoutParams.WRAP_CONTENT;

       lp.height = LayoutParams.WRAP_CONTENT;

       lp.flags = LayoutParams.FLAG_NOT_FOCUSABLE

               | LayoutParams.FLAG_NOT_TOUCH_MODAL;

       return lp;

    }

·  然后在新建的Service的onStartCommand()函数中增加对installFloatingWindow()的调用。

·  在应用程序的主Activity的onCreate()函数中调用startService()以启动这个服务。

·  在应用程序的AndroidManifest.xml中增加对权限android.permission.SYSTEM_ALERT_WINDOW的使用声明。

当完成这些工作之后,运行这个应用即可得到如图6-1所示的效果。一个名为“Clickme to dismiss!”的按钮浮在其他应用之上。而点击这个按钮后,它便消失了。

 

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第1张图片

图 6 - 1浮动窗口例子的运行效果

读者可以将本例与第4章的例子SampleWindow做一个对比。它们的实现效果是大同小异的。而然,本章的这个例子无论是从最终效果、代码量、API的复杂度或可读性上都有很大的优势。这得益于对控件系统的使用。在这里,控件Button托管了窗口的绘制过程,并且将输入事件封装为了更具可读性的回调。并且添加窗口时所使用的WindowManager实例掩盖了客户端与WMS交互的复杂性。更重要的是,本例所使用的接口都来自公开的API,也就是说可以脱离Android源码进行编译。这无疑会带来更方便的开发过程以及更好的程序兼容性。

因此,除非需要进行很底层的窗口控制,使用本例所介绍的方法向系统中添加窗口是最优的选择。

6.1.2 控件系统的组成

从这个例子中可以看到在添加窗口过程中的两个关键组件:Button和WindowManager。Button是控件的一种,继承自View类。不只Button,任何一个继承自View类的控件都可以作为一个窗口添加到系统中去。WindowManager其实是一个继承自ViewManager的接口,它提供了添加/删除窗口,更新窗口布局的API,可以看作是WMS在客户端的代理类。不过WindowManager的接口与WMS的接口相差很大,几乎已经无法通过WindowManager看到WMS的模样。这也说明了WindowManager为了精简WMS的接口做过大量的工作。这部分内容也是本章的重点。

因此控件系统便可以分为继承自View类的一系列控件类与WindowManager两个部分。

6.2 深入理解WindowManager

WindowManager的主要功能是提供简单的API使得使用者可以方便地将一个控件作为一个窗口添加到系统中。本节将探讨它工作原理。

6.2.1 WindowManager的创建与体系结构

首先需要搞清楚WindowManager是什么。

准确的说,WindowManager是一个继承自ViewManager的接口。ViewManager定义了三个函数,分别用于添加/删除一个控件,以及更新控件的布局。

ViewManager接口的另一个实现者是ViewGroup,它是容器类控件的基类,用于将一组控件容纳到自身的区域中,这一组控件被称为子控件。ViewGroup可以根据子控件的布局参数(LayoutParams)在其自身的区域中对子控件进行布局。

读者可以将WindowManager与ViewGroup进行一下类比:设想WindowManager是一个ViewGroup,其区域为整个屏幕,而其中的各个窗口就是一个一个的View。WindowManager通过WMS的帮助将这些View按照其布局参数(LayoutParams)将其显示到屏幕的特定位置。二者的核心工作是一样的,因此WindowManager与ViewGroup都继承自ViewManager。

接下来看一下WindowManager接口的实现者。本章最开始的例子通过Context.getSystemService(Context.WINDOW_SERVICE)的方式获取了一个WindowManager的实例,其实现如下:

[ContextImpl.java-->ContextImpl.getSystemService()]

public Object getSystemService(String name) {

    // 获取WINDOW_SERVICE所对应的ServiceFetcher

   ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);

    // 调用fetcher.getService()获取一个实例

    returnfetcher == null ? null : fetcher.getService(this);

}

Context的实现者ContextImpl在其静态构造函数中初始化了一系列的ServiceFetcher来响应getSystemService()的调用并创建对应的服务实例。看一下WINDOW_SERVICE所对应的ServiceFetcher的实现:

[ContextImpl.java-->ContextImpl.static()]

registerService(WINDOW_SERVICE, new ServiceFetcher() {

           public Object getService(ContextImpl ctx) {

               // ① 获取Context中所保存的Display对象

               Display display = ctx.mDisplay;

               /* ② 倘若Context中没有保存任何Display对象,则通过DisplayManager获取系统

                  主屏幕所对应的Display对象 */

               if (display == null) {

                   DisplayManager dm =

                            (DisplayManager)ctx.getOuterContext().getSystemService(Context.DISPLAY_SERVICE);

                   display = dm.getDisplay(Display.DEFAULT_DISPLAY);

               }

               // ③ 使用Display对象作为构造函数创建一个WindowManagerImpl对象并返回

               return new WindowManagerImpl(display);

           }});

由此可见,通过Context.getSystemService()的方式获取的WindowManager其实是WindowManagerImpl类的一个实例。这个实例的构造依赖于一个Display对象。第4章介绍过DisplayContent的概念,它在WMS中表示一块的屏幕。而这里的Display对象与DisplayContent的意义是一样的,也用来表示一块屏幕。

再看一下WindowManagerImpl的构造函数

[WindowManagerImpl.java-->WindowManagerImpl.WindowManagerImpl()]

    publicWindowManagerImpl(Display display) {

       this(display, null);

    }

 

    privateWindowManagerImpl(Display display, Window parentWindow) {

       mDisplay = display;

       mParentWindow = parentWindow;

    }

其构造函数实在是出奇的简单,仅仅初始化了mDisplay与mParentWindow两个成员变量而已。从这两个成员变量的名字与类型来推断,它们将决定通过这个WindowManagerImpl实例所添加的窗口的归属。

WindowManagerImpl的构造函数引入了一个Window类型的参数parentWindow。Window类是什么呢?以Activity为例,一个Activity显示在屏幕上时包含了标题栏、菜单按钮等控件,但是在setContentView()时并没有在layout中放置它们。这是因为Window类预先为我们准备好了这一切,它们被称之为窗口装饰。除了产生窗口装饰之外,Window类还保存了窗口相关的一些重要信息。例如窗口ID(IWindow.asBinder()的返回值)以及窗口所属Activity的ID(即AppToken)。在6.6.1 介将会对这个类做详细的介绍。

也许在WindowManagerImpl的addView()函数的实现中可以找到更多的信息。

[WindowManagerImpl.java-->WindowManagerImpl.addView()]

    publicvoid addView(View view, ViewGroup.LayoutParams params) {

       mGlobal.addView(view, params, mDisplay, mParentWindow);

    }

WindowManagerImpl.addView()将实际的操作委托给一个名为mGlobal的成员来完成,它随着WindowManagerImpl的创建而被初始化:

    privatefinal WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

可见mGlobal的类型是WindowManagerGlobal,而且WindowManagerGlobal是一个单例模式——即一个进程中最多仅有一个WindowManagerGlobal实例。所有WindowManagerImpl都是这个进程唯一的WindowManagerGlobal实例的代理。

此时便对WindowManager的结构体系有了一个清晰的认识,如图6-2所示。

 

图 6 - 2 WindowManager的结构体系

·  ViewManager接口:WindowManager体系中最基本的接口。WindowManager继承自这个接口说明了WindowManager与ViewGroup本质上的一致性。

·  WindowManager接口:WindowManager接口继承自ViewManager接口的同时,根据窗口的一些特殊性增加了两个新的接口。getDefaultDisplay()用以得知这个WindowManager的实例会将窗口添加到哪个屏幕上去。而removeViewImmediate()则要求WindowManager必须在这个调用返回之前完成所有的销毁工作。

·  WindowManagerImpl类:WindowManager接口的实现者。它自身没有什么实际的逻辑,WindowManager所定义的接口都是交由WindowManagerGlobal完成的。但是它保存了两个重要的只读成员,它们分别指明了通过这个WindowManagerImpl实例所管理的窗口将被显示在哪个屏幕上以及将会作为哪个窗口的子窗口。因此在一个进程中,WindowManagerImpl的实例可能有多个。

·  WindowManagerGlobal类:它没有继承上述任何一个接口,但它是WindowManager的最终实现者。它维护了当前进程中所有已经添加到系统中的窗口的信息。另外,在一个进程中仅有一个WindowManagerGlobal的实例。

在理清了WindowManager的结构体系后,便可以探讨WindowManager是如何完成窗口管理的。其管理方式体现在其对ViewManager的三个接口的实现上。为了简洁起见,我们将直接分析WindowManagerGlobal中的实现。

6.2.2 通过WindowManagerGlobal添加窗口

参考WindowManagerGlobal.addView()的代码:

[WindowManagerGlobal.java-->WindowManagerGlobal.addView()]

    publicvoid addView(View view, ViewGroup.LayoutParams params,

            Display display, Window parentWindow){

        ......// 参数检查

 

       final WindowManager.LayoutParams wparams =(WindowManager.LayoutParams)params;

 

        /* ① 如果当前窗口需要被添加为另一个窗口的附属窗口(子窗口),则需要让父窗口视自己的情况

            对当前窗口的布局参数(LayoutParams)进行一些修改 */

        if(parentWindow != null) {

           parentWindow.adjustLayoutParamsForSubWindow(wparams);

        }

 

       ViewRootImpl root;

        ViewpanelParentView = null;

 

       synchronized (mLock) {

            ......

 

           // WindowManager不允许同一个View被添加两次

           int index = findViewLocked(view, false);

           if (index >= 0) { throw new IllegalStateException("......");}

 

           // ② 创建一个ViewRootImpl对象并保存在root变量中

           root = new ViewRootImpl(view.getContext(), display);

 

           view.setLayoutParams(wparams);

 

           /* ③ 将作为窗口的控件、布局参数以及新建的ViewRootImpl以相同的索引值保存在三个

              数组中。到这步为止,我们可以认为完成了窗口信息的添加工作 */

           mViews[index] = view;

            mRoots[index] = root;   //ViewRootImpl

           mParams[index] = wparams;

        }

 

        try{

           /* ④ 将新加窗口的控件设置给ViewRootImpl。这个动作将导致ViewRootImpl向WMS

               添加新的窗口、申请Surface以及托管控件在Surface上的重绘动作。这才是真正意义上

                完成了窗口的添加操作*/

           root.setView(view, wparams, panelParentView);

        }catch (RuntimeException e) { ...... }

    }

添加窗口的代码并不复杂。其中的关键点有:

#·  父窗口修改新窗口的布局参数可能修改的只有LayoutParams.token和LayoutParams.mTitle两个属性mTitle属性不必赘述,仅用于调试。而token属性则值得一提。回顾一下第4章的内容,每一个新窗口必须通过LayoutParams.token向WMS出示相应的令牌才可以在addView()函数中通过父窗口修改这个token属性的目的是为了减少开发者的负担。开发者不需要关心token到底应该被设置为 什么值,只需将LayoutParams丢给一个WindowManager,剩下的事情就不用再关心了
父窗口修改token属性的原则是如果新窗口的类型为子窗口(其类型大于等于LayoutParams.FIRST_SUB_WINDOW并小于等于LayoutParams.LAST_SUB_WINDOW),则LayoutParams.token所持有的令牌为其父窗口的ID(也就是IWindow.asBinder()的返回值)否则LayoutParams.token将被修改为父窗口所属的Activity的ID(也就是在第4章中所介绍的AppToken),这对类型为TYPE_APPLICATION的新窗口来说非常重要。
从这点来说,当且仅当新窗的类型为子窗口时,addView()的parentWindow参数才是真正意义上的父窗口。这类子窗口有上下文菜单、弹出式菜单以及游标等等在WMS中,这些窗口对应的WindowState所保存的mAttachedWindow既是parentWindow所对应的WindowState。然而另外还有一些窗口,如对话框窗口,类型为TYPE_APPLICATION, 并不属于子窗口,但需要AppToken作为其令牌,为此parentWindow将自己的AppToken赋予了新窗口的的LayoutParams.token中。此时parentWindow便并不是严格意义上的父窗口了。

#·  为新窗口创建一个ViewRootImpl对象。顾名思义,ViewRootImpl实现了一个控件树的根它负责与WMS进行直接的通讯,负责管理Surface,负责触发控件的测量与布局,负责触发控件的绘制,同时也是输入事件的中转站。总之,ViewRootImpl是整个控件系统正常运转的动力所在,无疑是本章最关键的一个组件。

#·  将控件、布局参数以及新建的ViewRootImpl以相同的索引值添加到三个对应的数组mViews、mParams以及mRoots中,以供之后的查询之需。控件、布局参数以及ViewRootImpl三者共同组成了客户端的一个窗口。或者说,在控件系统中的窗口就是控件、布局参数与ViewRootImpl对象的一个三元组。

注意 笔者并不认同将这个三元组分别存储在三个数组中的设计。如果创建一个WindowRecord类来统一保存这个三元组将可以省去很多麻烦。

另外,mViews、mParams以及mRoots这三个数组的容量是随着当前进程中的窗口数量的变化而变化的。因此在addView()以及随后的removeView()中都伴随着数组的新建、拷贝等操作。鉴于一个进程所添加的窗口数量不会太多,而且也不会很频繁,所以这些时间开销是可以接受的。不过笔者仍然认为相对于数组,ArrayList或CopyOnWriteArrayList是更好的选择。

#·  调用ViewRootImpl.setView()函数,将控件交给ViewRootImpl进行托管。这个动作将使得ViewRootImpl向WMS添加窗口、获取Surface以及重绘等一系列的操作。这一步是控件能够作为一个窗口显示在屏幕上的根本原因!

总体来说,WindowManagerGlobal在通过父窗口调整了布局参数之后,将新建的ViewRootImpl、控件以及布局参数保存在自己的三个数组中,然后将控件交由新建的ViewRootImpl进行托管,从而完成了窗口的添加。WindowManagerGlobal管理窗口的原理如图6-3所示。

 

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第2张图片

                                                                         图 6 - 3 WindowManagerGlobal的窗口管理

6.2.3 更新窗口的布局

ViewManager所定义的另外一个功能就是更新View的布局。在WindowManager中,则是更新窗口的布局。窗口的布局参数发生变化时,如LayoutParams.width从100变为了200,则需要将这个变化通知给WMS使其调整Surface的大小,并让窗口进行重绘。这个工作在WindowManagerGlobal中由updateViewLayout()函数完成。

[WindowManagerGlobal.java-->WindowManagerGlobal.updateViewLayout()]

    publicvoid updateViewLayout(View view, ViewGroup.LayoutParams params) {

        ......// 参数检查

       final WindowManager.LayoutParams wparams =(WindowManager.LayoutParams)params;

        // 将布局参数保存到控件中

       view.setLayoutParams(wparams);

 

       synchronized (mLock) {

           // 获取窗口在三个数组中的索引

           int index = findViewLocked(view, true);

           ViewRootImpl root = mRoots[index];

            // 更新布局参数到数组中

           mParams[index] = wparams;

           // 调用ViewRootImpl的setLayoutParams()使得新的布局参数生效

           root.setLayoutParams(wparams, false);

        }

    }

更新窗口布局的工作在WindowManagerGlobal中是非常简单的,主要是保存新的布局参数,然后调用ViewRootImpl.setLayoutParams()进行更新。

6.2.3 删除窗口

接下来探讨窗口的删除操作。在了解了WindowManagerGlobal管理窗口的方式后应该可以很容易地推断出删除窗口所需要做的工作:

·  从3个数组中删除此窗口所对应的元素,包括控件、布局参数以及ViewRootImpl。

·  要求ViewRootImpl从WMS中删除对应的窗口(IWindow),并释放一切需要回收的资源。

这个过程十分简单,这里就不引用相关的代码了。只是有一点需要说明一下:要求ViewRootImpl从WMS中删除窗口并释放资源的方法是调用ViewRootImpl.die()函数。因此可以得出这样一个结论:ViewRootImpl的生命从setView()开始,到die()结束。

6.2.4 WindowManager的总结

经过前文的分析,相信读者对WindowManager的工作原理有了深入的认识。

·  鉴于窗口布局和控件布局的一致性,WindowManager继承并实现了接口ViewManager。

·  使用者可以通过Context.getSystemService(Context.WINDOW_SERVICE)来获取一个WindowManager的实例。这个实例的真实类型是WindowManagerImpl。WindowManagerImpl一旦被创建就确定了通过它所创建的窗口所属哪块屏幕?哪个父窗口?

·  WindowManagerImpl除了保存了窗口所属的屏幕以及父窗口以外,没有任何实质性的工作。窗口的管理都交由WindowManagerGlobal的实例完成。

·  WindowManagerGlobal在一个进程中只有一个实例。

·  WindowManagerGlobal在3个数组中统一管理整个进程中的所有窗口的信息。这些信息包括控件、布局参数以及ViewRootImpl三个元素。

·  除了管理窗口的上述3个元素以外,WindowManagerGlobal将窗口的创建、销毁与布局更新等任务交付给了ViewRootImpl完成。

说明 在实际的应用开发过程中,有时会在logcat的输出中遇到有关WindowLeaked的异常输出。WindowLeaked异常发生与WindowManagerGlobal中,其原因是Activity在destroy之前没有销毁其附属窗口,如对话框、弹出菜单等。

如此看来,WindowManager的实现仍然是很轻量的。窗口的创建、销毁与布局更新都指向了一个组件:ViewRootImpl。

6.3 深入理解ViewRootImpl

ViewRootImpl实现了ViewParent接口,作为整个控件树的根部,它是控件树正常运作的动力所在,控件的测量、布局、绘制以及输入事件的派发处理都由ViewRootImpl触发。另一方面,它是WindowManagerGlobal工作的实际实现者,因此它还需要负责与WMS交互通信以调整窗口的位置大小,以及对来自WMS的事件(如窗口尺寸改变等)作出相应的处理。

本节将对ViewRootImpl的实现做深入的探讨。

6.3.1 ViewRootImpl的创建及其重要的成员

ViewRootImpl创建于WindowManagerGlobal的addView()方法中,而调用addView()方法的线程即是此ViewRootImpl所掌控的控件树UI线程ViewRootImpl的构造主要是初始化了一些重要的成员,事先对这些重要的成员有个初步的认识对随后探讨ViewRootImpl的工作原理有很大的帮助。其构造函数代码如下:

[ViewRootImpl.java-->ViewRootImpl.ViewRootImpl()]

public ViewRootImpl(Context context, Displaydisplay) {

 

    /* ① 从WindowManagerGlobal中获取一个IWindowSession的实例。它是ViewRootImpl和

      WMS进行通信的代理 */

    mWindowSession= WindowManagerGlobal.getWindowSession(context.getMainLooper());

    // ②保存参数display,在后面setView()将会把窗口添加到这个Display上

    mDisplay= display;

 

   CompatibilityInfoHolder cih = display.getCompatibilityInfo();

   mCompatibilityInfo = cih != null ? cih : new CompatibilityInfoHolder();

 

    /* ③ 保存当前线程到mThread。这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主线程。

      在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检

      查发起请求的thread与这个mThread是否相同。倘若不同则会拒绝这个请求并抛出一个异常*/

    mThread= Thread.currentThread();

    ......

 

    /* ④ mDirty用于收集窗口中的无效区域。所谓无效区域是指由于数据或状态发生改变时而需要进行重绘

      的区域。举例说明,当应用程序修改了一个TextView的文字时,TextView会将自己的区域标记为无效

      区域,并通过invalidate()方法将这块区域收集到这里的mDirty中。当下次绘制时,TextView便

      可以将新的文字绘制在这块区域上 */

    mDirty =new Rect();

   mTempRect = new Rect();

    mVisRect= new Rect();

 

    /* ⑤ mWinFrame,描述了当前窗口的位置和尺寸。与WMS中WindowState.mFrame保持着一致 */

   mWinFrame = new Rect();

 

    /* ⑥ 创建一个W类型的实例(Bn端),W是IWindow.Stub的子类。即它将在WMS中作为新窗口的ID,以及接

       收来自WMS的回调*/

    mWindow= new W(this);

    ......

    /* ⑦ 创建mAttachInfomAttachInfo是控件系统中很重要的对象。它存储了此当前控件树所贴附

      的窗口的各种有用的信息,并且会派发给控件树中的每一个控件这些控件会将这个对象保存在自己的

     mAttachInfo变量中。mAttachInfo中所保存的信息有WindowSession,窗口的实例(即mWindow),

      ViewRootImpl实例,窗口所属的Display,窗口的Surface以及窗口在屏幕上的位置等等所以,当

      要需在一个View中查询与当前窗口相关的信息时非常值得在mAttachInfo中搜索一下 */

   mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display,this, mHandler, this);

 

    /* ⑧ 创建FallbackEventHandler。这个类如同PhoneWindowManger一样定义在android.policy

      包中,其实现为PhoneFallbackEventHandler。FallbackEventHandler是一个处理未经任何人

      消费的输入事件。在6.5.4节中将会介绍它 */

   mFallbackEventHandler =PolicyManager.makeNewFallbackEventHandler(context);

    ......

 

    /* ⑨ 创建一个依附于当前线程(主线程)的Choreographer用于通过VSYNC特性安排重绘行为 */

    mChoreographer= Choreographer.getInstance();

    ......

}

在构造函数之外,还有另外两个重要的成员被直接初始化:

·  mHandler,类型为ViewRootHandler,一个依附于创建ViewRootImpl的线程,即主线程上的,用于将某些必须主线程进行的操作安排在主线程中执行mHandler与mChoreographer的同时存在看似有些重复,其实它们拥有明确不同的分工与意义由于mChoreographer处理消息时具有VSYNC特性,因此它主要用于处理与重绘相关的操作。但是由于mChoreographer需要等待VSYNC的垂直同步事件来触发对下一条消息的处理因此它处理消息的及时性稍逊于mHandler而mHandler的作用,则是为了将发生在其他线程中的事件安排在主线程上执行。所谓发生在其他线程中的事件是指来自于WMS由继承自IWindow.Stub的mWindow引发的回调。由于mWindow是一个Binder对象的Bn端,因此这些回调发生在Binder的线程池中。而这些回调会影响到控件系统的重新测量、布局与绘制,因此需要此Handler将回调安排到主线程中。

[说明] mHandler与mThread两个成员都是为了单线程模型而存在的。Android的UI操作不是线程安全的,而且很多操作也是建立在单线程的假设之上(如scheduleTraversals())。采用单线程模型的目的是降低系统的复杂度,并且降低锁的开销。

·  mSurface,类型为Surface。采用无参构造函数创建的一个Surface实例。mSurface此时是一个没有任何内容的空壳子,在 WMS通过relayoutWindow()为其分配一块Surface之前尚不能使用。

·  mWinFrame、mPendingContentInset、mPendingVisibleInset以及mWidth,mHeight。这几个成员存储了窗口布局相关的信息。其中mWinFrame、mPendingConentInsets、mPendingVisibleInsets与窗口在WMS中的Frame、ContentInsets、VisibleInsets是保持同步的这是因为这3个成员不仅会作为 relayoutWindow()的传出参数,而且ViewRootImpl在收到来自WMS的回调IWindow.Stub.resize()时,立即更新这3个成员的取值。因此这3个成员体现了窗口在WMS中的最新状态。
与mWinFrame中的记录窗口在WMS中的尺寸不同的是,mWidth/mHeight记录了窗口在ViewRootImpl中的尺寸,二者在绝大多数情况下是相同的。当窗口在WMS中被重新布局而导致尺寸发生变化时,mWinFrame会首先被IWindow.Stub.resize()回调更新,此时mWinFrame便会与mWidth/mHeight产生差异此时ViewRootImpl即可得知需要对控件树进行重新布局以适应新的窗口变化。在布局完成后,mWidth/mHeight会被赋值为mWinFrame中所保存的宽和高,二者重新统一。在随后分析performTraversals()方法时,读者将会看到这一处理。另外,与mWidth/mHeight类似,ViewRootImpl也保存了窗口的位置信息Left/Top以及ContentInsets/VisibleInsets供控件树查询,不过这四项信息被保存在了mAttachInfo中。

ViewRootImpl的在其构造函数中初始化了一系列的成员变量,然而其创建过程仍未完成。仅在为其指定了一个控件树进行管理,并向WMS添加了一个新的窗口之后,ViewRootImpl承上启下的角色才算完全确立下来。因此需要进一步分析ViewRootImpl.setView()方法。

[ViewRootImp.java-->ViewRootImpl.setView()]

public void setView(View view,WindowManager.LayoutParams attrs, View panelParentView) {

   synchronized (this) {

        if (mView == null) {

           // ① mView保存了控件树的根

           mView = view;

           ......

           // ②mWindowAttributes保存了窗口所对应的LayoutParams

           mWindowAttributes.copyFrom(attrs);

            ......

 

           /* 在添加窗口之前,先通过requestLayout()方法在主线程上安排一次“遍历”。所谓

             “遍历”是指ViewRootImpl中的核心方法performTraversals()。这个方法实现了对

              控件树进行测量、布局、向WMS申请修改窗口属性以及重绘的所有工作。由于此“遍历”

              操作对于初次遍历做了一些特殊处理,而来自WMS通过mWindow发生的回调会导致一些属性

              发生变化,如窗口的尺寸、Insets以及窗口焦点等,从而有可能使得初次“遍历”的现场遭

              到破坏。因此,需要在添加窗口之前,先发送一个“遍历”消息到主线程。

               在主线程中向主线程的Handler发送消息如果使用得当,可以产生很精妙的效果。例如本例

              中可以实现如下的执行顺序:添加窗口->初次遍历->处理来自WMS的回调 */

           requestLayout();

 

           /*③ 初始化mInputChannel参考第五章,InputChannel是窗口接受来自InputDispatcher

             的输入事件的管道。 注意,仅当窗口的属性inputFeatures不含有

             INPUT_FEATURE_NO_INPUT_CHANNEL时才会创建InputChannel,否则mInputChannel

             为空,从而导致此窗口无法接受任何输入事件 */

           if ((mWindowAttributes.inputFeatures

                   & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {

               mInputChannel = new InputChannel();

           }

 

           try {

               ......

               /* 将窗口添加到WMS中。完成这个操作之后,mWindow已经被添加到指定的Display中去

                 而且mInputChannel(如果不为空)已经准备好接受事件了。只是由于这个窗口没有进行

                 过relayout(),因此它还没有有效的Surface可以进行绘制 */

               res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,

                        getHostVisibility(), mDisplay.getDisplayId(),

                       mAttachInfo.mContentInsets, mInputChannel);

           } catch (RemoteException e) {......} finally { ...... }

           ......

           if (res < WindowManagerGlobal.ADD_OKAY) {

               // 错误处理。窗口添加失败的原因通常是权限问题,重复添加,或者tokeen无效

           }

           ......

            /*④ 如果mInputChannel不为空,则创建mInputEventReceiver,用于接受输入事件。

             注意第二个参数传递的是Looper.myLooper(),即mInputEventReceiver将在主线程上

             触发输入事件的读取与onInputEvent()。这是应用程序可以在onTouch()等事件响应中

             直接进行UI操作等根本原因。

            */

           if (mInputChannel != null) {

               ......

               mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,

                           Looper.myLooper());

           }

 

          /* ViewRootImpl将作为新加view的parent。所以,ViewRootImpl可以从控件树中任何一个

            控件开始,通过回溯getParent()的方法得到 */

           view.assignParent(this);

            ......

        }

    }

}

至此,ViewRootImpl所有重要的成员都已经初始化完毕,新的窗口也已经添加到WMSViewRootImpl的创建过程是由构造函数和setView()方法两个环节构成的。其中构造函数主要进行成员的初始化,setView()则是创建窗口、建立输入事件接收机制的场所。同时,触发第一次“遍历”操作的消息已经发送给主线程,在随后的第一次“遍历”完成后,ViewRootImpl将会完成对控件树的第一次测量、布局,并从WMS获取窗口的Surface以进行控件树的初次绘制工作。

在本节的最后,通过图 6 – 4对ViewRootImpl中的重要成员进行了分类整理。

 

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第3张图片

                                                                             图 6 - 4 ViewRootImpl中的主要成员

6.3.2 控件系统的心跳:performTraversals()

ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发“遍历”操作的消息,“遍历”操作是指performTraversals()方法。它的性质与WMS中的performLayoutAndPlaceSurfacesLocked()类似,是一个包罗万象的方法。ViewRootImpl中接收到的各种变化,如来自WMS的窗口属性变化,来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用,并在其中完成处理。View类及其子类中的onMeasure()、onLayout()以及onDraw()等回调也都是在performTraversals()的执行过程中直接或间接地引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊地工作着,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此,performTraversals()函数可谓是ViewRootImpl的心跳。

由于布局的相关工作是此方法中最主要的内容,为了简化分析,并突出此方法的工作流程,本节将以布局的相关工作为主线进行探讨。待完成了这部分内容的分析之后,庞大的performTraversals()方法将不再那么难以驯服,读者便可以轻易地学习其他的工作了。

1.performTraversals()的工作阶段

performTraversals()是Android 源码中最庞大的方法之一,因此在正式探讨它的实现之前最好先将其划分为以下几个工作阶段作为指导。

·  预测量阶段。这是进入performTraversals()方法后的第一个阶段,它会控件树进行第一次测量。测量结果可以通过mView. getMeasuredWidth()、Height()获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次得到回调。

·  布局窗口阶段。根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。

·  最终测量阶段。预测量的结果是控件树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多(参考第4章),WMS不一定会将窗口准确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。

·  布局控件树阶段。完成最终测量之后便可以对控件树进行布局了。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。

·  绘制阶段。这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。

[说明] 很多文章都倾向于将performTraversals()的工作划分为测量、布局与绘制三个阶段。然而笔者认为如此划分隐藏了WMS在这个过程中的地位,并且没能体现出控件树对窗口尺寸的期望WMS对窗口尺寸做最终的确定最后以WMS给出的结果为准再次进行测量的协商过程而这个协商过程充分体现了ViewRootImpl作为WMS与控件树的中间人的角色。

接下来将结合代码,对上述五个阶段进行深入的分析。

2.预测量与测量原理

本节将探讨performTraversals()将以何种方式对控件树进行预测量,同时,本节也会对控件的测量过程与原理进行介绍。

预测量参数的候选

预测量也是一次完整的测量过程,它与最终测量的区别仅在于参数不同而已实际的测量工作在View或其子类的onMeasure()方法中完成,并且其测量结果需要受限于来自其父控件的指示这个指示由onMeasure()方法的两个参数进行传达:widthSpec与heightSpec。它们是被称为MeasureSpec的复合整型变量,用于指导控件对自身进行测量。它有两个分量,结构如图6-5所示。

 

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第4张图片

                                        图 6 - 5 MeasureSpec的结构

其1到30位给出了父控件建议尺寸。建议尺寸对测量结果的影响依不同的SPEC_MODE的不同而不同SPEC_MODE的取值取决于此控件的LayoutParams.width/height的设置,可以是如下三种值之一。

·  MeasureSpec.UNSPECIFIED (0)表示控件在进行测量时,可以无视SPEC_SIZE的值。控件可以是它所期望的任意尺寸。

·  MeasureSpec.EXACTLY (1):表示子控件必须为SPEC_SIZE所制定的尺寸。当控件的LayoutParams.width/height为一确定值,或者是MATCH_PARENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。

· MeasureSpec.AT_MOST (2)表示子控件可以是它所期望的尺寸,但是不得大于SPEC_SIZE。当控件的LayoutParams.width/height为WRAP_CONTENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。

Android提供了一个MeasureSpec类用于组合两个分量成为一个MeasureSpec,或者从MeasureSpec中分离任何一个分量。

那么ViewRootImpl会如何为控件树的根mView准备其MeasureSpec呢?

参考如下代码,注意desiredWindowWidth/Height的取值,它们将是SPEC_SIZE分量的候选。另外,这段代码分析中也解释了与测量无关,但是比较重要的代码段。

[ViewRootImpl.java-->ViewRootImpl.performTraversals()]

private void performTraversals() {

    // 将mView保存在局部变量host中,以此提高对mView的访问效率

    finalView host = mView;

    ......

    // 声明本阶段的主角,这两个变量将是mView的SPEC_SIZE分量的候选

    int desiredWindowWidth;

    int desiredWindowHeight;

    .......

    Rect frame = mWinFrame; // 如上一节所述,mWinFrame表示了窗口的最新尺寸

 

    if(mFirst) {

        /*mFirst表示了这是第一次遍历,此时窗口刚刚被添加到WMS,此时窗口尚未进行relayout,因此

          mWinFrame中没有存储有效地窗口尺寸 */

        if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {

            ......// 为状态栏设置desiredWindowWidth/Height,其取值是屏幕尺寸

        }else {

            //① 第一次“遍历”的测量,采用了应用可以使用的最大尺寸(屏幕宽高)作为SPEC_SIZE的候选

           DisplayMetrics packageMetrics =

               mView.getContext().getResources().getDisplayMetrics();

           desiredWindowWidth = packageMetrics.widthPixels;

           desiredWindowHeight = packageMetrics.heightPixels;

        }

        /* 由于这是第一次进行“遍历”,控件树即将第一次被显示在窗口上,因此接下来的代码填充

         mAttachInfo中的一些字段然后通过mView发起了dispatchAttachedToWindow()的调用

          之后每一个位于控件树中的控件都会回调onAttachedToWindow() */

        ......

    } else {

        // ② 在非第一次遍历的情况下,会采用窗口的最新尺寸作为SPEC_SIZE的候选

       desiredWindowWidth = frame.width();

       desiredWindowHeight = frame.height();

 

        /* 如果窗口的最新尺寸ViewRootImpl中的现有尺寸不同,说明WMS侧单方面改变了窗口的尺寸

          这将产生如下三个结果 */

        if(desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {

            // 需要进行完整的重绘以适应新的窗口尺寸

           mFullRedrawNeeded = true;

           // 需要对控件树进行重新布局

           mLayoutRequested = true;

           /* 控件树有可能拒绝接受新的窗口尺寸比如在随后的预测量中给出了不同于窗口尺寸的测量结果

             产生这种情况时,就需要在窗口布局阶段尝试设置新的窗口尺寸 */

           windowSizeMayChange = true;

        }

    }

    ......

    /* 执行位于RunQueue中的回调。RunQueue是ViewRootImpl的一个静态成员,即是说它是进程唯一

      的,并且可以在进程的任何位置访问RunQueue。在进行多线程任务时,开发者可以通过调用View.post()

      或View.postDelayed()方法将一个Runnable对象发送到主线程执行。这两个方法的原理是将

      Runnable对象发送到ViewRootImpl的mHandler去。当控件已经加入到控件树时,可以通过

     AttachInfo轻易获取这个Handler。而当控件没有位于控件树中时,则没有mAttachInfo可用,此时

      执行View.post()/PostDelay()方法,Runnable将会被添加到这个RunQueue队列中。

      在这里,ViewRootImpl将会把RunQueue中的Runnable发送到mHandler中,进而得到执行。所以

      无论控件是否显示在控件树中,View.post()/postDelay()方法都是可用的,除非当前进程中没有任何

      处于活动状态的ViewRootImpl */

   getRunQueue().executeActions(attachInfo.mHandler);

 

    booleanlayoutRequested = mLayoutRequested && !mStopped;

 

    /* 仅当layoutRequested为true时才进行预测量。

     layoutRequested为true表示在进行“遍历”之前requestLayout()方法被调用过。

     requestLayout()方法用于要求ViewRootImpl进行一次“遍历”并对控件树重新进行测量与布局 */

    if(layoutRequested) {

       final Resources res = mView.getContext().getResources();

 

        if(mFirst) {

            ......// 确定控件树是否需要进入TouchMode,本章将在6.5.1节介绍 TouchMode

        }else {

            /*检查WMS是否单方面改变了ContentInsetsVisibleInsets。注意对二者的处理的差异,

             ContentInsets描述了控件在布局时必须预留的空间,这样会影响控件树的布局,因此将

             insetsChanged标记为true,以此作为是否进行控件布局的条件之一。而VisibleInsets

             描述了被遮挡的空间,ViewRootImpl在进行绘制时,需要调整绘制位置以保证关键控件或区域,

             如正在进行输入的TextView等不被遮挡,这样VisibleInsets的变化并不会导致重新布局,

             所以这里仅仅是将VisibleInsets保存到mAttachInfo中,以便绘制时使用 */

           if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {

               insetsChanged = true;

           }

           if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {

               mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);

           }

 

            /*当窗口的width或height被指定为WRAP_CONTENT时,表示这是一个悬浮窗口(没有父窗口)。

             此时会对desiredWindowWidth/Height进行调整在前面的代码中,这两个值被设置

             被设置为窗口的当前尺寸。而根据MeasureSpec的要求,测量结果不得大于SPEC_SIZE。

             然而,如果这个悬浮窗口需要更大的尺寸以完整显示其内容时,例如为AlertDialog设置了

             一个很长的消息内容,如此取值将导致无法得到足够大的测量结果,从而导致内容无法完整显示。

             因此,对于此等类型的窗口,ViewRootImpl会调整desiredWindowWidth/Height为此应用

             可以使用的最大尺寸 */

           if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT

                   || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {

               // 悬浮窗口的尺寸取决于测量结果。因此有可能需要向WMS申请改变窗口的尺寸。

               windowSizeMayChange = true;

 

               if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {

                   //

               } else {

                   // ③ 设置悬浮窗口SPEC_SIZE的候选为应用可以使用的最大尺寸

                   DisplayMetrics packageMetrics = res.getDisplayMetrics();

                   desiredWindowWidth = packageMetrics.widthPixels;

                   desiredWindowHeight = packageMetrics.heightPixels;

               }

           }

        }

 

        // ④ 进行预测量。通过measureHierarchy()方法 对desiredWindowWidth/Height进行测量

        windowSizeMayChange |=measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);

    }

 

    // 其他阶段的处理

    ......

由此可知,预测量时的SPEC_SIZE按照如下原则进行取值

·  第一次“遍历”时,使用应用可用的最大尺寸作为SPEC_SIZE的候选。

·  此窗口是一个悬浮窗口,即LayoutParams.width/height其中之一被指定为WRAP_CONTENT时,使用应用可用的最大尺寸作为SPEC_SIZE的候选。

·  在其他情况下,使用窗口最新尺寸作为SPEC_SIZE的候选。

最后,通过measureHierarchy()方法进行测量。

测量协商

measureHierarchy()(测量阶层)用于测量整个控件树。传入的参数desiredWindowWidth与desiredWindowHeight在前述代码中根据不同的情况作了精心的挑选。控件树本可以按照这desiredWindowWidth与desiredWindowHeight参数完成测量,但是measureHierarchy()有自己的考量,即如何将窗口布局地尽可能地优雅。

这是针对将LayoutParams.width设置为了WRAP_CONTENT的悬浮窗口而言。如前文所述,在设置为WRAP_CONTENT时,指定的desiredWindowWidth是应用可用的最大宽度,如此可能会产生如图6-6左图所示的丑陋布局。这种情况较容易发生在AlertDialog中,当AlertDialog需要显示一条比较长的消息时,由于给予的宽度足够大,因此它有可能将这条消息以一行显示,并使得其窗口充满了整个屏幕宽度,在横屏模式下这种布局尤为丑陋。

倘若能够对可用宽度进行适当的限制,迫使AlertDialog将消息换行显示,则产生的布局结果将会优雅得多,如图6-6右图所示。但是,倘若不分清红皂白地对宽度进行限制,当控件树真正需要足够的横向空间时,会导致内容无法显示完全,或者无法达到最佳的显示效果。例如当一个悬浮窗口希望尽可能大地显示一张照片时就会出现这样的情况。

 

图 6 - 6 丑陋的布局与优雅的布局

那么measureHierarchy()如何解决这个问呢?它采取了与控件树进行协商的办法,即先使用measureHierarchy()所期望的宽度限制尝试对控件树进行测量然后通过测量结果来检查控件树是否能够在此限制下满足其充分显示内容的要求。倘若没能满足,则measureHierarchy()进行让步,放宽对宽度的限制,然后再次进行测量,再做检查。倘若仍不能满足则再度进行让步。

参考代码如下:

[ViewRootImpl.java-->ViewRootImpl.measureHierarchy()]

private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp,

       final Resources res, final int desiredWindowWidth,

        final int desiredWindowHeight) {

    int childWidthMeasureSpec; // 合成后的用于描述宽度的MeasureSpec

    int childHeightMeasureSpec; // 合成后的用于描述高度的MeasureSpec

    boolean windowSizeMayChange = false; // 表示测量结果是否可能导致窗口的尺寸发生变化

    boolean goodMeasure = false; // goodMeasure表示了测量是否能满足控件树充分显示内容的要求

 

    // 测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下

    if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {

        /* ① 第一次协商。measureHierarchy()使用它最期望的宽度限制进行测量。这一宽度限制定义为

         一个系统资源可以在frameworks/base/core/res/res/values/config.xml找到它的定义 */

       res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true);

        intbaseSize = 0;

        // 宽度限制被存放在baseSize中

        if(mTmpValue.type == TypedValue.TYPE_DIMENSION) {

           baseSize = (int)mTmpValue.getDimension(packageMetrics);

        }

 

        if(baseSize != 0 && desiredWindowWidth > baseSize) {

           // 使用getRootMeasureSpec()函数组合SPEC_MODE与SPEC_SIZE为一个MeasureSpec

           childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

            childHeightMeasureSpec =

                           getRootMeasureSpec(desiredWindowHeight,lp.height);

            //②第一次测量。由performMeasure()方法完成

           performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

 

           /* 控件树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。如果

             控件树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位 */

           if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)==0) {

               goodMeasure = true; // 控件树对测量结果满意,测量完成

           } else {

               // ③ 第二次协商。上次测量结果表明控件树认为measureHierarchy()给予的宽度太小,

                 在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制 */

               baseSize = (baseSize+desiredWindowWidth)/2;

               childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

 

               // ④ 第二次测量

               performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

               // 再次检查控件树是否满足此次测量

               if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {

                   goodMeasure = true; // 控件树对测量结果满意,测量完成

               }

            }

        }

    }

 

    if(!goodMeasure) {

        /* ⑤ 最终测量。当控件树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制

          做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没

          有更多的空间供其使用了 */

       childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);

       childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);

       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

 

        /* 最后,如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口

          尺寸的调整 */

        if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())

        {

           windowSizeMayChange = true;

        }

    }

    // 返回窗口尺寸是否可能需要发生变化

    return windowSizeMayChange;

}

显然,对于非悬浮窗口,即当LayoutParams.width被设置为MATCH_PARENT时,不存在协商过程,直接使用给定的desiredWindowWidth/Height进行测量即可而对于悬浮窗口,measureHierarchy()可以连续进行两次让步。因而在最不利的情况下,在ViewRootImpl的一次“遍历”中,控件树需要进行三次测量,即控件树中的每一个View.onMeasure()会被连续调用三次之多,如图6-7所示。所以相对于onLayout(),onMeasure()方法的对性能的影响比较大。

 

                                                                        图 6 - 7 协商测量的三次尝试

接下来通过performMeasure()看控件树如何进行测量。

测量原理

performMeasure()方法的实现非常简单,它直接调用mView.measure()方法,将measureHierarchy()给予的widthSpec与heightSpec交给mView。

看下View.measure()方法的实现:

[View.java-->View.measure()]

public final void measure(int widthMeasureSpec,int heightMeasureSpec) {

    /* 仅当给予的MeasureSpec发生变化,或要求强制重新布局时,才会进行测量。

      所谓强制重新布局是指当控件树中的一个子控件的内容发生变化时,需要进行重新的测量和布局的情况

      在这种情况下,这个子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定与上次测量

      时的值相同,因而导致从ViewRootImpl到这个控件的路径上的父控件的measure()方法无法得到执行

      进而导致子控件无法重新测量其尺寸或布局因此,当子控件因内容发生变化时,从子控件沿着控件树回溯

      到ViewRootImpl,并依次调用沿途父控件的requestLayout()方法,在这个方法中,会在

     mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利

      执行,进而这个子控件有机会进行重新测量与布局。这便是强制重新布局的意义 */

    if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||

           widthMeasureSpec != mOldWidthMeasureSpec ||

           heightMeasureSpec != mOldHeightMeasureSpec) {

 

        /* ① 准备工作。从mPrivateFlags中将PFLAG_MEASURED_DIMENSION_SET标记去除。

         PFLAG_MEASURED_DIMENSION_SET标记用于检查控件在onMeasure()方法中是否通过

         调用setMeasuredDimension()将测量结果存储下来 */

       mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

       ......

        /* ② 对本控件进行测量 每个View子类都需要重载这个方法以便正确地对自身进行测量。

          View类的onMeasure()方法仅仅根据背景Drawable或style中设置的最小尺寸作为

          测量结果*/

       onMeasure(widthMeasureSpec, heightMeasureSpec);

 

        /* ③ 检查onMeasure()的实现是否调用了setMeasuredDimension()

         setMeasuredDimension()会将PFLAG_MEASURED_DIMENSION_SET标记重新加入

         mPrivateFlags中。之所以做这样的检查,是由于onMeasure()的实现可能由开发者完成,

          而在Android看来,开发者是不可信的 */

        if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET)

                                     !=PFLAG_MEASURED_DIMENSION_SET) {

           throw new IllegalStateException(......);

        }

 

       // ④ 将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags。这一操作会对随后的布局操作放行

       mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;

    }

 

    // 记录父控件给予的MeasureSpec,用以检查之后的测量操作是否有必要进行

   mOldWidthMeasureSpec = widthMeasureSpec;

   mOldHeightMeasureSpec = heightMeasureSpec;

}

从这段代码可以看出,View.measure()方法没有实现任何测量算法,它的作用在于引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。另外,在控件系统看来,一旦控件执行了测量操作,那么随后必须进行布局操作,因此在完成测量之后,将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags,以便View.layout()方法可以顺利进行。

onMeasure()的结果通过setMeasuredDimension()方法尽量保存。setMeasuredDimension()方法的实现如下:

[View.java-->View.setMeasuredDimension()]

protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) {

    /* ① 测量结果被分别保存在成员变量mMeasuredWidth与mMeasuredHeight中

   mMeasuredWidth = measuredWidth;

    mMeasuredHeight = measuredHeight;

    // ② 向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果

   mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

}

其实现再简单不过。存储测量结果的两个变量可以通过getMeasuredWidthAndState()与getMeasuredHeightAndState()两个方法获得,就像ViewRootImpl.measureHierarchy()中所做的一样。此方法虽然简单,但需要注意,与MeasureSpec类似,测量结果不仅仅是一个尺寸,而是一个测量状态与尺寸的复合整、变量其0至30位表示了测量结果的尺寸,而31、32位则表示了控件对测量结果是否满意,即父控件给予的MeasureSpec是否可以使得控件完整地显示其内容。当控件对测量结果满意时,直接将尺寸传递给setMeasuredDimension()即可,注意要保证31、32位为0倘若对测量结果不满意,则使用View.MEASURED_STATE_TOO_SMALL | measuredSize 作为参数传递给setMeasuredDimension()以告知父控件对MeasureSpec进行可能的调整。

既然明白了onMeasure()的调用如何发起,以及它如何将测量结果告知父控件,那么onMeasure()方法应当如何实现的呢?对于非ViewGroup的控件来说其实现相对简单,只要按照MeasureSpec的原则如实计算其所需的尺寸即可。而对于ViewGroup类型的控件来说情况则复杂得多,因为它不仅拥有自身需要显示的内容(如背景),它的子控件也是其需要测量的内容。因此它不仅需要计算自身显示内容所需的尺寸,还有考虑其一系列子控件的测量结果。为此它必须为每一个子控件准备MeasureSpec,并调用每一个子控件的measure()函数。

由于各种控件所实现的效果形形色色,开发者还可以根据需求自行开发新的控件,因此onMeasure()中的测量算法也会变化万千。不从Android系统实现的角度仍能得到如下的onMeasure()算法的一些实现原则

·  控件在进行测量时,控件需要将它的Padding尺寸计算在内,因为Padding是其尺寸的一部分。

·  ViewGroup在进行测量时,需要将子控件的Margin尺寸计算在内。因为子控件的Margin尺寸是父控件尺寸的一部分。

·  ViewGroup为子控件准备MeasureSpec时,SPEC_MODE应取决于子控件的LayoutParams.width/height的取值。取值为MATCH_PARENT或一个确定的尺寸时应为EXACTLY,WRAP_CONTENT时应为AT_MOST。至于SPEC_SIZE,应理解为ViewGroup对子控件尺寸的限制,即ViewGroup按照其实现意图所允许子控件获得的最大尺寸。并且需要扣除子控件的Margin尺寸。

·  虽然说测量的目的在于确定尺寸,与位置无关。但是子控件的位置是ViewGroup进行测量时必须要首先考虑的。因为子控件的位置即决定了子控件可用的剩余尺寸,也决定了父控件的尺寸(当父控件的LayoutParams.width/height为WRAP_CONTENT时)。

·  在测量结果中添加MEASURED_STATE_TOO_SMALL需要做到实事求是。当一个方向上的空间不足以显示其内容时应考虑利用另一个方向上的空间,例如对文字进行换行处理,因为添加这个标记有可能导致父控件对其进行重新测量从而降低效率。

·  当子控件的测量结果中包含MEASURED_STATE_TOO_SMALL标记时,只要有可能,父控件就应当调整给予子控件的MeasureSpec,并进行重新测量。倘若没有调整的余地,父控件也应当将MEASURED_STATE_TOO_SMALL加入到自己的测量结果中,让它的父控件尝试进行调整。

·  ViewGroup在测量子控件时必须调用子控件的measure()方法,而不能直接调用其onMeasure()方法。直接调用onMeasure()方法的最严重后果是子控件的PFLAG_LAYOUT_REQUIRED标识无法加入到mPrivateFlag中,从而导致子控件无法进行布局。

综上所述,测量控件树的实质是测量控件树的根控件。完成控件树的测量之后,ViewRootImpl便得知了控件树对窗口尺寸的需求。

(4)确定是否需要改变窗口尺寸

接下来回到performTraversals()方法。在performTraversals内部调用ViewRootImpl.measureHierarchy()执行完毕之后,ViewRootImpl了解了控件树所需的空间。于是便可确定是否需要改变窗口窗口尺寸以便满足控件树的空间要求。前述的measureHierarchy()代码中多处设置windowSizeMayChange变量为true。windowSizeMayChange仅表示有可能需要改变窗口尺寸而接下来的这段代码则用来确定窗口是否需要改变尺寸。

[ViewRootImpl.java-->ViewRootImp.performTraversals()]

private void performTraversals() {

    ......// 测量控件树的代码

    /* 标记mLayoutRequested为false。因此在此之后的代码中,倘若控件树中任何一个控件执行了

     requestLayout(),都会重新进行一次“遍历” */

    if (layoutRequested) {

        mLayoutRequested = false;

    }

 

    // 确定窗口是否确实需要进行尺寸的改变

    booleanwindowShouldResize = layoutRequested && windowSizeMayChange

       && ((mWidth != host.getMeasuredWidth() || mHeight !=host.getMeasuredHeight())

           || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&

                   frame.width() < desiredWindowWidth && frame.width() !=mWidth)

           || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&

                   frame.height() < desiredWindowHeight && frame.height() !=mHeight));

}

 

确定窗口尺寸是否确实需要改变的条件看起来比较复杂,这里进行一下总结,先介绍必要条件:

·  layoutRequested为true即ViewRootImpl.requestLayout()方法被调用过。View中也有requestLayout()方法。当控件内容发生变化从而需要调整其尺寸时,会调用其自身的requestLayout()并且此方法会沿着控件树向根部回溯,最终调用到ViewRootImp.requestLayout(),从而引发一次performTraversals()调用之所以这是一个必要条件,是因为performTraversals()还有可能因为控件需要重绘时被调用。当控件仅需要重绘而不需要重新布局时(例如背景色或前景色发生变化时),会通过invalidate()方法回溯到ViewRootImpl此时不会通过performTraversals()触发performTraversals()调用,而是通过scheduleTraversals()进行触发在这种情况下layoutRequested为false,即表示窗口尺寸不需发生变化

·  windowSizeMayChange为true,如前文所讨论的,这意味着WMS单方面改变了窗口尺寸,而控件树的测量结果与这一尺寸有差异,或当前窗口为悬浮窗口,其控件树的测量结果将决定窗口的新尺寸。

在满足上述两个条件的情况下,以下两个条件满足其一:

·  测量结果与ViewRootImpl中所保存的当前尺寸有差异。

·  悬浮窗口的测量结果与窗口的最新尺寸有差异。

注意:ViewRootImpl对是否需要调整窗口尺寸的判断是非常小心的。第4章介绍WMS的布局子系统时曾经介绍过,调整窗口尺寸所必须调用的performLayoutAndPlaceSurfacesLocked()函数会导致WMS对系统中的所有窗口新型重新布局,而且会引发至少一个动画帧渲染,其计算开销相当之大。因此ViewRootImpl仅在必要时才会惊动WMS。

至此,预测量阶段完成了。

总结

这一阶段的工作内容是为了给后续阶段做参数的准备并且其中最重要的工作是对控件树的预测量,至此ViewRootImpl得知了控件树对窗口尺寸的要求。另外,这一阶段还准备了后续阶段所需的其他参数:

·  viewVisibilityChanged。即View的可见性是否发生了变化。由于mView是窗口的内容,因此mView的可见性即是窗口的可见性。当这一属性发生变化时,需要通过通过WMS改变窗口的可见性。

LayoutParams预测量阶段需要收集应用到LayoutParams的改动,这些改动一方面来自于WindowManager.updateViewLayout(),而另一方面则来自于控件树以SystemUIVisibility为例,View.setSystemUIVisibility()所修改的设置需要反映到LayoutParams中,而这些设置确却保存在控件自己的成员变量里。在预测量阶段会通过ViewRootImpl.collectViewAttributes()方法遍历控件树中的所有控件以收集这些设置,然后更新LayoutParams。

3.窗口布局窗口与最终测量
      接下来进入窗口布局阶段与最终测量阶段。窗口布局阶段以 relayoutWindow()方法为核
心,并根据布局结果进行相应处理。而当布局结果使得窗口尺寸发生改变时,最终测量阶段
将会被执行。最终测量使用 performMeasure()方法完成,因此其过程与预测量完全一致,区
别仅在于 MeasureSpec参数的不同
,所以本小节将一并探讨这两个阶段。另外,由于布局窗
口会对 Surface产生影响,这个阶段中会出现与硬件加速相关的代码。关于硬件加速的详细内
容将在6.4节中进行讨论,而在本节中,读者仅需了解 Surface的状态以及窗口尺寸对硬件加
速的影响即可。

(1)布局窗口的条件

      如前文所述,窗口布局的开销很大,因此必须限制窗口布局阶段的执行。另外,倘若不
需要进行窗口布局,则WMS不会在预测量之后修改窗口的尺寸。在这种情况下预测量的结
果是有效的,因此不再需要进行最终测量。参考如下代码:
[ViewRootImpl.java-->ViewRootImpl.performTraversals()]
private void performTraversals(){
    ......//预测量阶段的代码
    // 进行布局窗口的4个条件
    if (mFirst || windowshouldResize || insetschanged ||
                viewvisibilitychanged || params != null) {
        ......//布局窗口与最终测量阶段代码
    }else
        /*倘若不符合执行布局窗口的条件,则说明窗口的尺寸不需要进行调整。在这种情况下,有可能是
          窗口的位置发生了变换,于是仅需将窗口的最新位置保存到 mAttachinfo中
*/
        final boolean windowMoved =(attachInfo.mwindowLeft || frame.left || attachInfo.mWindowTop != frame.top);
        if (windowMoved){
            ......
            attachInfo.mwindowLeft= frame.left;
            attachInfo.mwindowTop = frame.top;
        }
    }
    //布局控件树阶段与绘制阶段代码
    ......
}
在进行窗口布局时,以下4个条件满足其一即进入布局窗口阶段,此4个条件的意义
如下
      口  mFirst,即表示这是窗口创建以来的第一次“遍历”,此时窗口仅仅是添加到WMs
            中,但尚未进行窗口布局,并且没有有效的 Surface进行内容绘制。
因此必须进行窗口布局。
      口  windowShouldResize,正如在预测量阶段所述,当控件树的测量结果与窗口的当前尺
            寸有差异时,需要通过布局窗口阶段向WMS提出修改窗口尺寸的请求以满足控件树的要求

      口  insetsChanged,表示WMS单方面改变了窗口的 ContentInsets这种情况一般发生在
            SystemUI的可见性发生了变化或输入法窗口弹出或关闭的情况下(请参考第4章)。

            严格来说,在这种情况下不需要重新进行窗口布局,只不过当 ContentInsets发生变化
            时,需要执行一段渐变动画使窗口的内容过渡到新的 ContentInsets下,而这段动画的
            启动动作发生在窗口布局阶段。稍后的代码分析中将介绍 ContentInsets的影响,以及
            这段动画的实现。

      口  params!=null,在进入 performTraversals()方法时, params变量被设置为mul。当窗口
            的使用者通过 WindowManager.updateViewLayout()函数修改窗口的 LayoutParams,或
            者在预测量阶段通过 collectViewAttributes()函数收集到的控件属性使得 LayoutParams
            发生变化时, params将被设置为新的 LayoutParams,此时需要将新的 LayoutParams
            通过窗口布局更新到WMS中使其对窗口依照新的属性进行重新布局

当上述4个条件全部不满足时,表示窗口布局是不必要的,而且窗口的尺寸也没有发生
变化,因此仅需将窗口的新位置(如果发生了变化)更新到 mAttachInfo中以供控件树查询

(2)布局窗口前的准备工作

参考如下代码:
[ViewRootlmpl.java-->ViewRootImp.performTraversals()]
private void performTraversals(){
    .......//预测量阶段
    if(/*引发布局窗口的4个条件*/){
        //①记录下在布局窗口之前是否拥有一块有效的 Surface
        boolean hadsurface= mSurface.isValid();
        try{
            //②记录下在布局窗口之前 Surface的版本号
            final int surfaceGenerationId = mSurface.getGenerationId();
            //③通过relayoutWindow()方法布局窗口
            relayoutResult = relayoutwindow(params, viewvisibility, insetspending);
            ..... //处理布局窗口产生的变化Part1
        }catch {...…:}
        ....../ /处理布局窗口产生的变化Part1
    }else {......}
    ......//布局控件树阶段与绘制阶段
}
上述代码体现了在布局窗口前做两个准备工作
      口  hadSurface,保存在布局窗口之前是否拥有一个有效的 Surface。当窗口第一次进行
            遍历”,或当前正处于不可见状态(mView的 Visibility为 INVISIBLE或GONE时不
            存在有效的 Surface。此变量可以在完成窗口布局后决定是否初始化或销毁用于绘制
            的 HardwareRenderer
      口  surfaceGenerationld,即 Surface的版本号。每当WMS为窗口重新分配 Surface时都
            会使得 Surface的版本号增加。当完成窗口布局后 Surface的版本号发生改变,在原
            Surface上建立的 HardwareRenderer以及在其上进行的绘制都将无效,因此此变量用
            于决定窗口布局后是否需要将 Surface重新设置到 HardwareRenderer以及是否需要进
            行一次完整绘制。

可以看出,布局窗口的准备工作目的是保存布局前的状态,以便在布局后判断状态变化
并做出相应处理。其实 Surface两个状态的存储只是准备工作的一部分,有些用于判断状态变
化的属性已经隐式地准备好了,它们有 mWidth/mHeight、 mAttachInfo.mLeft/mTop/mContent
Insets/m Visiblelnsets等。在布局完成后,它们将同 hadSurface与 surfaceGenerationId一起决定
布局后的工作。

(3)布局窗口

ViewRootlmpl使用 relayoutwindow()方法进行窗口布局,参考以下代码来实现。
[ViewRootimp. java-->ViewRootlmp.relayoutWindow()]
private int relayoutwindow (WindowManager Layout Params params, int viewvisibility
                            boolean insets Pending) throws RemoteException {
    ......
    int relayoutResult = mwindowSession. relayout(
                                        mwindow, msec, param,
                                        (int) (mview. getMeasuredwidth()* appscale 0.5f),
                                        (int) (mview, getMeasuredHeight()* appscale 0.5f),
                                        viewvisibility,
                                        insetsPending? windowManagerGlobal.RELAYOUT_INSETS_PENDING:0,
                                        mwinFrame, mPendingContentInsets, mPendingvisibleInsets,
                                        mPendingconfiguration, mSur face);
    ......
    return relayoutResult
}
relayoutWindow()是 IWindowSession,.relayout(),即 WMW.relayoutWindow()的包装方法。
它将窗口的 LayoutParams、预测量时的结果以及mvew的可见性作为输入,并将 mWinFrame、
mPendingContentinsets/VisibleInsets、 mSurface作为输出。

      relayoutWindow()并没有直接将预测的结果交给WMS,而是乘以了appScale这个
系数, appScale用于在兼容模式下显示一个窗口。当窗口在设备的屏幕尺寸下显示异常时
Android会尝试使用兼容尺寸显示它(例如320×480),此时测量与布局控件树
都将以此兼容尺寸为准。为了充分利用屏幕,或避免窗口内容显示在屏幕外, Android
计算了用以使兼容尺寸变換到屏幕尺寸的一个缩放系数,即 appScale。这时窗口测量
控件树的布局都将以兼容尺寸进行以保证布局的正确性,而生成 Surface,在绘制过程
将会使用 appScale进行缩放,以保证最终显示的内容能够充分利用屏幕。

另外,传出参数 mPendingConfiguration在之前的章节中并没有做过详细介绍。作为
个 Configuration类型的实例,其意义是WMS给予窗口的当前配置。其中的字段
描述了设备当前的语言、屏幕尺寸、输入方式(触屏或键盘)、UI模式(夜间模式、
车載模式等)、dpi等。其中WMS有可能更改的最常用的字段是 orientation,即屏幕
方向。

(4)布局窗口后的处理— Insets

      relayoutWindow()方法完成布局窗口后,回到 performTraversals()方法,对布局结果所导
致的变化进行处理,首先是 ContentInsets和Visiblelnsets。参考如下代码:
[ViewRootlmpl.java-->ViewRootlmp.performTraversals()]
private void performTraversals(){
    //①对比布局结果检查 InsetS是否发生了变化
    contentInsetschanged = !mPendingContentInsets.equals(mAttachInfo.mContentInsets);
    visibleInsetschanged = !mPendingVisibleInsets.equals(mAttachInfo. mVisibleInsets);
    //如果 Contentinseta发生变化
    if (contentInsetsChanged) {
        /*启动过渡动画以避免内容发生突兀的抖动。其条件非常多,总结一下
          1> 布局窗口之前 mWidth/ mHeight有效,即之前曾经完成过布局,即布局有效,此时可以依照
                Insets发生变化前的市局进行绘制。
          2> systerUIvisibility没有指定要求隐藏状态栏或导航栏。因为当指定了此类 systemUIVisibility后
               控件树布局时将不会考虑 Contentinsets,而是充满屏幕
          3> 拥有有效的 Surface,这个条件是不言而喻的
          4> 此窗口采用硬件加速方式进行绘制,并且其 HardwareRenderer处于有效状态。因为
               过渡动画是以硬件加速方式实现的·
          5> 窗口的 Surface不得支持透明度。因为 Android当前的硬件实现不支持在支持透明度的
               Surface上进行透明度变换。而这个过渡动画正是一个透明度动画 */

        /*启动一个透明度动画,使得 Contentinsete发生变化时产生的画面移位不那么突兀。在介绍硬件
          加速绘制之后再讨论这一动画的细节*/
        .......
        //②将最新的 ContentInsets保存到 mAttachInto中
        mAttachInfo.mcontentInsets.set(mpendingcontentInsets);
    }
    if (contentInsetschanged || mlastsystemUivisibility !=
                    mAttachInfo.mSystemUiVisibility || mEitSyaterWindowsRequested){
        mLastsystemuivisibility = mAttachInfo msystemuivisibility;
        mFitsystemNindowsRequested = false ;
        mFitSystemWindowsInsets. set(mAttachInfo.mContentInsets);
        /*③要求mView及其子控件适应这一 ContentInsets
          view.fitsystemwindow()将会把 Contentinsets作为其 Padding属性保存下来

          Padding是指控件的边界到其内容边界的距离。在测量布局及绘制时需琴将 Padding属性
          计算在内 */
        host.fitsystemwindows(mFitsystemilindowsInsets);
    }
    /*④如果visibleInsets发生变化,将其保存到 mAttachInto中
      如前所述, visibleInsets不影响测量与布局,面仅仅影响绘制时的偏移,因此除了将其保存下来
      供绘制时使用以外无须进行其他操作
*/
    if (visibleInsetschanged)(
        mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
    }
    ......
}
可见,当 ContentInsets发生变化时,会进行如下动作
      口  启动一个过渡动画以避免突兀的画面位移。参考图6-8,当状态栏被隐藏时窗口顶
            部的 Contentinsets为0,内容布局在屏幕的顶部。当状态栏重新显示时,窗口顶部的
            Contentinsets为状态栏的高度,此时内容将会向下错位,布局在状态栏的底部。在用
            户看来, Contentinsets变化所导致的是窗口内容的突兀抖动,因此 ViewRootlmpl通过
            一个过渡动画使得画面从左图以渐变的方式过渡到右图以减轻这种突兀感。
这一动画
            的渲染过程将在6.4节介绍
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第5张图片

      口  保存新的 Contentlnsets到 mAttachInfo
      口  通过执行 mView.fitSystemWindows()使控件树应用新的 ContentInsets。其实质是设
            置控件的 Padding属性以便在其进行测量、布局与绘制时让出 ContentInsets所指定
            的空间。从此方法在 ViewGroup中的实现可以看出它与 onMeasure()一样会沿着控
            件树遍历给所有的控件。
过在 Activity、 Dialog等由 Android辅助添加的窗口中
            此操作会被其控件树的根控件 Decorview阻断,即 Decorview完成其 Padding设置
            后便不会再将此操作传递给其子控件。其合理性在于 Decorview作为整个控件树的
            根,只要它设置好 Padding值并在测量、布局以及绘制时让出 Contentlnsets指定的
            空间,其子控件必然会位于 ContentInsets之外,因此子控件不需要对 Contentinsets
            进行处理。
当使用 WindowManager.addView()方法添加一个控件作为窗口时,可以
            调用此控件的 makeOptionalFitsSystemWindows()方法使此控件拥有 Decorview的这
            一特性。

而当 Visiblelnsets发生变化时则简单得多,仅需保存新的值即可。在绘制前会根据此值计
算绘制偏移,以保证关键控件或区域位于可视区域之内。

(5)布局窗口后的处理— Surface

      在完成 Insets的处理之后,便会处理 Surface在窗口布局过程中的变化。这里将会用到在
准备工作中所保存的状态 hadSurface与 surfaceGenerationld
[ViewRootlmpl. java-> ViewRootlmpl.performTraversals()]
private void performTraversals () {
    ......
    if ( hadSurface){
        if(mSurface.isValid()){
            /*①布局窗口之前没有有效的 Surface,而布局窗口之后有了。
              当此窗口启用硬件加速时( mHardwareRenderer!=null),将使用新的 Surface初始化用
              于硬件渲染的 HardwareRenderer:第一次“遍历”或窗口从不可见变为可见时符合此种情况
*/
            ......
            if (mAttachInfo. mHardwareRenderer != null){
                try{
                    hwInitialized=mAttachInfo. mHardwareRenderer.initialize(mHolder.getSurface());
                } catch (surface.outofResource eXception e) {.....}
            }
        }
    } else if (!mSurface isvalid())(
        /*②布局窗口之前拥有有效的Surface,但布局窗口之后没有了
          当窗口启用硬件加速时则销毁当前使用的 HardwareRenderer,因为不需要再进行绘制。
          当窗口从可见变为不可见成窗口被移除时符合此种悄况
*/
        if (mAttachInfo.mHardwareRenderer != null &&
                    mAttachInfo.mHardwareRenderer.isEnabled()){
            mAttachInfo.mHardwareRenderer.destroy(true);
        }
    }else if (surface GenerationId ! sUrface getGenerationId( &&
                    mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null){
        ......
        /*③经过窗口布局后, Surface发生了改变。
          底层的 Surface被置换了,此时需要将 Surface重新设置给 HardwareRencerer以将其与底层新的
          Surface进行绑定
*/
        try{
            mAttachInfo.mHardwareRenderer.updatesurface(mHolder.getSurface();
        } catch (Surface. OutOfResourcesException e){......}
    }
}
显然,处理 Surface的变化是为了硬件加速而服务的其原因在于以软件方式进行绘制
时可以通过 Surface. lockCanvas()函数直接获得,因此仅需在绘制前判断一下 Surface.isvalid()
决定是否绘制即可。
而在硬件加速绘制的情况下,将绘制过程委托给 HardwareRenderer
并且需要将其与一个有效的 Surface进行绑定
。因此每当 Surface的状态变换都需要通知
HardwareRenderer。软件与硬件加速的绘制原理将在64节中介绍
      另外,代码中的 mHolder是一个空的 Surfaceholder子类的实例,其 getsurfaceO方法所
返回的 Surface就是 mSurface

      在这代码之后,还有一段类似的代码处理Surface状态的变化,并将状态变化通过
一个SurfaceHolder汇报给mSurfaceHolderCallback,这段代码为了支持一种叫
NativeActivity的机制而实现的。 NativeActivity是一类 Activity,其逻辑实现由C++完成并
由NDK进行编译。由于 NativeActivity可以直接访问 Surface, ViewRootimpl需要通
过这段代码通知其 Surface的狀态变化

(6)布局窗口后的处理—更新尺寸

      接下来, ViewRootImpl会保存最新的窗口位置与尺寸,同时,如果窗口采用了硬件加速
则更新其坐标系以适应新的窗口尺寸。

[ViewRootImpl.java-->ViewRootlmpl.performTraversals()]
private void performTraversals(){
    ......
    //①保存窗口的位置与尺寸信息
    attachInfo.mWindowleft = frame.left;
    attachInfo.mWindowTop = frame.top;
    if (mwidth != frame.width() ||  mHeight != frame.height()){
        mWidth = frame.width();
        mHeight = frame.height();
    }
    ......
    /*②更新尺寸信息到 HardwareRenderer。 HardwareRenderer的 setup()方法将会使用此宽高信息
      来设置其 ViewPort,这样便可以在 Surface上建立(0,0, mWidth, mHeight)的坐标系
“/
    if (mAttachInfo.mHardwareRenderer !=null && 
            mAttachInfo.mHardwareRenderer.isEnabled()){
        if(hwInitialized || windowShouldResize ||
                mWidth != mAttachInfo.mHardwareRenderer.getwidth() ||
                mHeight ! mAttachInfo.mHardwareRenderer.getHeight()){
            mAttachInfo.mHardwareRenderer.setup(width, mHeight);
            if(!hwInitialized){
                mAttachInfo.mHardwareRenderer.invalidate(mHolder.getsurface());
                mFullRedrawNeeded = true;
            }
        }
    }
    ......
}
在最终的窗口位置与尺寸得到更新之后,便进入下一阶段—窗口的最终测量。

(7)最终测量

      最终测量阶段与预测量阶段一样使用预测量阶段所介绍的 performMeasure()进行,而
区别在于参数不同。
预测量使用了屏幕的可用空间或窗口的当前尺寸作为候选,使用
measureHierarchy()方法以协商的方式确定 MeasureSpec参数,并且其测量结果体现了控件树
所期望的窗口尺寸。在窗口布局时这一期望尺寸交给WMS以期能将窗口重新布局为这一尺
寸,然而由于WMS布局窗口时所考虑的因素很多,这一期望不一定如愿。在最终测量阶段
控件树将被迫接受WMS的布局结果,以最新的窗口尺寸作为 MeasureSpec参数进行测量。

测量结果将是随后布局阶段中设置控件边界时的依据。参考这一阶段的代码
[ViewRootilmpl.java->ViewRootlmpl.performTraversals()]
private void perform Traversals(){
    if (! mStopped) {
        //窗口布局还会影响另一个状态的变化: TouchMode,其相关内容在6.5节中介绍
        boolean focuschangedDueToTouchMode = 
                        ensureTouchModeLocally((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
        /*①进行最终测量的条件: 
                TouchMode发生变化;
                最新的窗口尺寸不符合预测量的结果; 
                ContentInsets发生变化(导致 Padding发生变化)
*/
        if (focuschangedDueToTouchMode || wWidth != host.getMeasuredwidth()
            || mHeight != host.getMeasuredHeight() || contentInsets changed){
            //②最终测量的参数为窗口的最新尺寸
            int childwidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight,lp.height);
            //③使用与预测量相同的 performMeasure()方法进行
            performMeasure(childwidthMeasure Spec, childHeightMeasurespec);
            ......
        }
    }
    ......
}
      之所以最终测量使用 perform Measure()而不是 measureHierarchy(),是因为
measureHierarchy包含协商的算法,以期确定最佳的测量尺寸。而在最终测量中,窗口尺寸已然确
定下来,是没有协商余地的。

(8)总结

      布局窗口与最终测量两个阶段至此便分析完毕。布局窗口阶段得以进行的原因控件系
有修改窗口属性的需求,如第一次“遍历”需要确定窗口的尺寸以及一块 Surface
,预测量
结果与窗口当前尺寸不一致需要进行窗口尺寸更改
, mView可见性发生变化需要将窗口隐藏
显示
, LayoutParams发生变化需要WMS以新的参数进行重新布局。而最终测量阶段得以
进行的原因
是窗口布局阶段确定的窗口尺寸与控件树的期望尺寸不一致,控件树需要对窗口
尺寸进行妥协

      完成这两个阶段之后, performTraversals()中剩余的变数已所剩无几,窗口的尺寸、控件
树中控件的尺寸都已最终确定。接下来便是控件树的布局与绘制了。

4.布局控件树阶段

       经过前面的测量,控件树中的控件对于自己的尺寸显然已经了然于胸,而且父控件对于
子控件的位置也有了眉目(因为为子控件准备 MeasureSpec时有可能需要计算子控件的位置)。
布局阶段将会把测量结果付诸行动,即把测量结果转化为控件的实际位置与尺寸。控件的实
际位置与尺寸由View的mLeft、mTop、 mRight以及 mBottom4个成员变量存储的坐标值来
表示
。因此,控件树的布局过程就是根据测量结果为每一个控件设置这4个成员变量的过程

      必须时刻注意一个事实:mLeft、mTop、 mRight以及 mBottom这些坐标值是相对于其
父控件的左上角的距离,也就是说这些坐标值是以父控件左上角为坐标原点进行计算
。另一种说法是这些坐标位于父控件的坐标系中。倘若需要获取控件在窗口坐标系
中的位置可以使用View. getLocationInWindow()方法
,相应,也可以通过 View. getLocationonScreen()
方法获取控件在屏幕坐标系下的位置。
这两个方法的实现原理是一个沿
着控件树向根部进行递归调用,其递归过程可以简单总结为控件在窗口中的位置等于
其在父窗口中的位置加上父窗口在窗口中的位置。

参考如下代码:
[ViewRootImpl.java-->ViewRootImpl.performTraversals()]
private void performTraversals() {
    ...... //前序阶段的代码
    //①布局控件树阶段的条件是layoutRequested
    final boolean didLayout = layoutRequested && !mStopped;
    if( didLayout){
        //②通过 performLayout()方法进行控件树的布局
        performLayout();
        /*③如果有必要,计算窗口的透明区城,并将此透明区域设置给WMS。
          计算透明区城的条件是 mView的mPriwateFlags中存在一个特定的标记。
*/
        if ((host.mPrivateFLags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) !=0){
            host. getLocationInwindow(mTmpLocation);
            //透明区域被初始化为整个mView的区域
            mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
                                    mTmpLocation[0]+ host.mRight - host.mLeft,
                                    mTmpLocation[1]+ host.mBottom - host.mTop);
            /*通过 gatherTransparentRegion()遍历控件树中的每一个控件,倘若控件有内容需要绘制
              则会将其所在区域从 mTransparentRegion中剪除
*/
            host.gatherTransparentRegion(mTransparentRegion);
            /*mTransparentRegion目前位于窗口坐标系中, mTranslator将此区域映射到屏幕坐标系中
              这么做的原因是WMS管理窗口是在屏幕坐标系中进行的
*/
            if (mTranslator ! null) {
                mTranslator.translateRegionInwindowToScreen(mTransparentRegion);
            }
        }
       //将透明区城设置到WMS
        if (! mTransparentRegion.equals(mPreviousTransparentRegion)) {
            mPreviousTransparentRegion.set(mTransparentRegion);
            try {
                mWiNdowSession.setTransparentRegion(mWindow, mTransparentRegion);
            } catch (RemoteException){}
        }
    }
    ...... // 绘制阶段的代码
}
       布局控件树的条件并不像布局窗口那么严格。只要 layoutRequested为true,即调用过
requestLayout()方法即可。 requestLayout()的用途在于当一个控件因为某种原因(如内容的尺
寸发生变化)而需要调整自身尺寸时,向 ViewRootlmpl申请进行一次新的“遍历”以便使此
控件得到一次新的测量布局与绘制
。所以,只要 requestLayout()被调用,则布局控件树阶段一定
会执行。而requestlayout()是否引发布局窗口阶段则取决于前述的4个条件是否满足

      为什么当一个控件调整尺寸时需要通过 requestLayout()使 ViewRootlmpl对整个控件树都
做同样的事情
呢?从之前阶段的分析可知,一个控件的测量结果可能直接影响控件树上各个
父控件的测量结果,甚至是窗口的布局。所以为了能够完整地处理一个控件的变化所产生的
影响, ViewRootlmpl都会将整个过程从头来一遍
这可能会引发对运行效率的担心,不过不
用担心, requestLayout()所调用的用于引发一次“遍历”的 sheduleTraversals()会检查是否在
主线程上已经安排了一次“遍历”。因此,倘若在一段代码中一次性地调用10个TextView的
setText函数可能会导致 requestLayout被调用10次,而 scheduleTraversa()的检查则会保
证随后仅执行一次“遍历”

布局控件树阶段主要做了两件事情:
      口  进行控件树布局。
      口  设置窗口的透明区域。

(1)控件树布局

      ViewRootImpl使用 performLayout()方法进行控件树布局,参考以下代码实现:
[VIew. java-->ViewRootlmpl.performLayout()]
private void performLayout ()[
    final View host = mView;
    try{
        //调用 mView.layout()方法启动布局
        host.layout(0,0,hast.getMeasuredWidth(),host.getMeasuredHeight());
    } finally {......}
}
此方法的实现非常简单,直接调用 mView.layout()方法
[View. java-->View.layout()]
public void layout (int. l, int t, int r, int b)
    //保存原始坐标
    int oldL = mLeft
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight
    //① setFram()方法将l、t、r、b分别设置到mLeft、mTop、mRight与 mBottom
    boolean changed = setFrame(l, t, r, b);
    /*是否还记得 PFLAG_LAYOUT_REQUIRED标记?它在View.measure()方法中被添加到 mPrivaterFlags。
      按照常理来说,当此控件的布局没有发生改变时是没有必要继续对子控件进行布局的,而这个标记则会
      将其放行,以保证真正需要布局的子控件得到布局
*/
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        /*②执行 onLayout()。如果这是一个ViewGroup, onLayout()中需要依次调用子控件的layout()方法*/
        onLayout(changed,I,t,r,b);
        //清除 PELAG_LAYOUT_REQUIRED标记
        mPrivateFlags &= -PFLAG LAYOUT REQUIRED;
        /*③通知每一个对此控件的布局变化感兴趣的监听者
        ListenerInfo li = mListenerinfo:
        if (li != null && li.mOnLayoutchangeListeners != null) {
            ArrayList listenerscopy =
                (ArrayList)li. mOnLayoutChangeListeners.clone ();
            int numListeners = listenerscopy.size();
            for (int i = 0; i < numListeners; ++i){
                listenersCopy get(i).onLayoutChange (this, l, tr rr b, old, oldT, oldR, oldB);
            }
        }
    }
}
Layouth方法主要做了三件事情:
      口  通过 seFrame()设置布局的4个坐标。
      口  调用 onLayout()方法,使子类得到布局变更的通知。如果此类是一个 ViewGroup,则
            需要在 onLayou()方法中依次调用每一个子控件的 layout()方法使其得到布局。切记
            不能调用子控件的 onLayout方法,这会导致子控件没有机会调用 setFrame(),从而
            使得此控件的坐标信息无法得到更新。

      口  通知每一个对此控件的布局变化感兴趣的监听者。可以通过调用
            View. addonLayoutChangeListener()加入对此控件的监听

      通知监听者的代码与控件树布局的核心逻辑无关,仍然将其帖附在这里的原因是由
于它的实现很值得开发者借鉴。注意到ayout()方法必定会在主线程中被 performTraversals()
调用,而 addOnLayoutChangeListener()没有限定调用线程,却没有增加任
何同步锁的保护。那么如何保证两个方法对 mListenerInfo访问的同步呢?
layout的
策略是将 mListenerInfo通过 clone()做一份拷贝,然后遍历这份拷贝,从而避免遍历
过程中 mListenerInfo发生变化而导致的越界。 ArrayList内部也采用类似的方法来保证
线程安全。

      另外,既然已经有 onLayout()方法监听布局的变化,为什么还需要监听者呢? onLayout()
有它的局限性,即只能在类内部访问,因此它更适合做类内部的监听与处理。而监听
者则给予类外部的对象监听其内部状态变化的能力,二者并不重复

对比测量与布局两个过程有助于加深对它们的理解
口  测量确定的是控件的尺寸,并在一定程度上确定了子控件的位置。而布局则是针对测
      量结果来实施,并最终确定子控件的位置。
口  测量结果对布局过程没有约束力。虽说子控件在 onMeasuret()方法中计算出了自己应
      有的尺寸,但是由于 layout()方法是由父控件调用,因此控件的位置尺寸的最终决定
      权在父控件手中,测量结果仅仅是一个参考
口  一般来说,子控件的测量结果影响父控件的测量结果,因此测量过程是后根遍历。而
      父控件的布局结果影响子控件的布局结果(例如位置),所以布局过程是先根遍历。
完成 performLayouto调用之后控件树的所有控件都已经确定了其最终位置,只等绘制了。

(2)窗口透明区域

      布局阶段另一个工作是计算并设置窗口的透明区域,这一功能主要是为 SurfaceView服务。
设想一个视频播放器的窗口,它包含一系列的控制按钮,位于主窗口上,而其进行视频
内容渲染的 SurfaceView所建立的子窗口则位于主窗口之下(参考第4章)。为了保证负责显
示视频的子窗口能够透过主窗口显示出来, Android引入了窗口透明区域的机制。
所谓的透明区域是指 Surface上的一块特定区域,在 SurfaceFlinger进行混层时, Surface
上的这个块区域将会被忽略,就好似在 Surface上切下一个洞一般
,如图6-9所示。
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第6张图片

      当控件树中存在 SurfaceView时,它会通过调用ⅤiewParent.requestTransparentRegion()方
法启用这一机制。这一方法的调用会沿着控件树回溯到 ViewRootImpl,并沿途将
PFLAG_REQUEST_TRANSPARENT_REGIONS标记加入父控件的 mPrivateFlags字段中。此标记会导
致ViewRootlmpl完成控件树的布局后将进行透明区域的计算与设置。

      透明区域的计算由View.gatherTransparentRegion()方法完成。透明区域的计算采用了挖洞
法,及默认整个窗口都是透明区域,在 gatherTransparentRegion()遍历到一个控件时,如果这
个此控件有内容需要绘制,则将其所在的区域从当前透明区域中删除,就好似在纸上裁出
个洞一样。当遍历完成后,剩余的区域就是最终的透明区域。

      这个透明区域将会被设置到WMS中,进而被WMS设置给 SurfaceFlinger,SurfaceFlinger
在进行 Surface的混合时,本窗口的透明区域部分将被忽略,从而用户能够透过这部分区域看
到后面窗口
(如 Surface View的窗口)的内容。

      作为透明区域的主要服务对象,在 gatherTransparentRegion()中 SurfaceView与其他类
型控件的做法正好相反。 SurfaceView会将其区域并到当前透明区域中。因此,先于
SurfaceView被遍历的控件所在的区域有可能被 SurfaceView所设置的透明区域覆盖
此时这些控件被覆盖的区域将不会被 Surface Flinger渲染。读者可以通过对比View类
与 SurfaceView类的gatherTransparentRegion()方法实现的差异加深对透明区域的理解

5.绘制阶段

      经历了前述4个阶段,每一个控件都已经确定好了自己的尺寸与位置,接下来就是最终
的绘制阶段。参考以下代码
[ ViewRootlmpl.java--> ViewRootimpl.performTraversals()]
public void performTraversals (){
    ...... //前4个阶段的代码
    boolean skipDraw false:
    if(mFirst){
        //第一次“遍历”时,会进行第一次焦点控件的计算。在介绍控件焦点的6.5.2节会介绍这一过程
        ......
        /*第一次“遍历”时,必然会进行窗口的重新布局以便确定窗口尺寸并获取一块 Surface。这意味
          着窗口将第一次可见,即所谓的出。在默认情况下,WMS会为窗口添加一个弹出动画,此时会在市局
          的运回值中增加 RELAYOUT_RES_ANIMATING标记。出于效率考虑,在窗口进行动画的过程中将跳过绘制
*/
        if((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_ANIMATING)!=0){
            mwindowsAnimating = true;
        }
    }else if (mwindowsAnimating)(
        //①如果窗口处在动画过程中,则跳过绘制阶段以提高动画的效率
        skipDraw = true;
    }
    mFirst = false;
    mWillDrawsoon = false;
    mNewSurfaceNeeded = false;
    mViewVisibility = viewVisibility;
    /*②确定是否需要向WMS发送染制完成的通知。第4章介绍过窗口的绘制状态,当窗口初次获得 Surface时其
      绘制状态被设置为 DRAW_PENDING,仅当WMS接收到窗口的 finishDrawingwindow()回调时,才会使
      窗口迁移到COMMIT_DRAW_PENDING,进而迁移到 READY_TO_SHOW
如果没有调用
      WMS.finishDrawingwindow(),即便在 Surface上绘制了内容,WMS也会因为窗口的绘制状态不为
       READY_TO_SHOW,而不会将窗口显示出来。

      mReportNextDraw是 ViewRootImpl用来确定是否需要向WMS发起 finishDrawingwindow()回调的条件。
      在这里,窗口初次获得了一块 Surface,此时窗口绘制状态必然为 DRAW_PENDING,因此将
      mReportNextDraw设置为true
*/
    if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0){
        mReportNextDraw = true;
    }
    //③当mView不可见时,自然也不需要进行绘制
    boolean cancelDraw = ...... || viewvisibility != View.VISIBLE;
    if(! cancelDraw && !newSurface){
        if(! skipDraw || mReportNextDraw) {
            ......
            //④performDraw()负责整个控件树的绘制
            performDraw();
        }
    }else{
        /*⑤如果窗口在此次“遍历”中获取了 Surface,则跳过本次“遍历”的绘制。
          并且通过 scheduleTraversals()方法重新做一次“遍历”,并在新的“遍历”中完成绘制
*/
        if (viewvisibility == View.VISIBLE)
            scheduleTraversals();
        }
        ......
    }
}
可见,同其他阶段一样,绘制也是有可能被跳过的。下面介绍跳过绘制的原因
      口  skipDraw:当窗口处于动画状态时, skipDraw会被置tue使得跳过绘制。在 Android
            看来,用户很容易注意到窗口动画的平滑性,因此它跳过了窗口的绘制使得更多
            的 CPU/GPU资源用来处理动画,在这个过程中窗口的内容是被冻结的。另外需要
            注意到, skipDraw的设置会因为 mReportNextDraw而失效。上面的代码分析中介
            绍 mReportNextDraw的作用是为了在窗口是 DRAW_PENDING状态时向WMS发起
            finishDrawingWindow()回调。因此 mReportNextDraw为true时窗口的 Surface尚未被
            显示出来并且没有任何内容。倘若此时不进行绘制工作会导致窗口迟迟不能迁移到
            COMMIT_DRAW_PENDING状态进而被显示出来,那么窗口动画也就无从谈起了

      口  canceldraw:当mView不可见时,自然也不需要进行绘制
      口  newSurface; newSurface表明窗口在本次“遍历”中获取了一块 Surface(可能由于这
            是第一次“遍历”或者 mView从不可见变为可见)。在这种情况下, ViewRootImpl选
            择通过调用 scheduleTraversals()在下次“遍历”中进行绘制,而不是在本次进行绘制。

可见,绘制阶段的限制条件相对于前4个阶段来说要宽松得多,在常态下只要
performTraversals()被调用,则一定会执行绘制阶段。

      performDraw()方法就是控件树的绘制入口。由于控件树的绘制十分复杂,因此, perform
DrawL方法的工作原理将在6.4节中单独介绍。

6. performTraversals()方法总结

      本节通过将 performTraversals()方法拆分为5个阶段进行详细介绍。图6-10以布局为主
线体现了这5个过程的基本流程关系。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第7张图片

      可见,前4个阶段以 layoutRequested为执行条件,即在“遍历”之前调用了 requestLayout()方法。
这是由于前4个阶段的主要工作目的是确定控件的位置与尺寸。因此,仅当一个或多个控件
有改变位置或尺寸的需求时(此时会调用 requestLayout())才有执行的必要


      即使 requestLayout()未被调用过,绘制阶段也会被执行。因为很多时候需要在不改
变控件位置与尺寸下进行重绘,例如某个控件改变了其文字颜色或背景色的情况下。与
requestLayout()方法相对,这种情况发生在控件调用了 invalidate()方法的时候。

      即便requestLayout()没有被调用过,有可能由于 LayoutParams、mView的可见性等与
窗口有关的属性发生变化时,“遍历”流程仍会进入第二阶段。由于这些属性与布局
没有直接联系,因此图6-10并没有体现这一点。

6.3.3 ViewRootImpl总结


      本节主要介绍了 ViewRootlmpl的创建,以及核心 performTraversals()方法的实现原
理。读者从中可以对 ViewRootlmpl架构与工作方式有深入理解。作为整个控件树的管理者
ViewRootlmpl十分复杂与庞大,不过其很多工作都会落在本节所介绍的5个阶段中完成,所
以深入理解这5个阶段的实现可以为学习 ViewRootImpl.的其他工作打下坚实的基础。在本章
后面的内容中将介绍控件树的绘制与输入事件派发两个方面的内容,那时会重新回到vew
Rootlmpl,探讨那些位于5个阶段之外的工作。

6.4深入理解控件树的绘制

      接下来将讨论控件系统中非常核心的内容—控件树的绘制。在开发 Android自定义控
件时,往往都需要重写View.onDraw()方法以绘制其内容到一个给定的 Canvas中,而且开发
者不需要知道控件以外的任何细节。本节将详细介绍 Android控件系统是如何为这个简单的
onDraw方法提供支持,以及隐藏在 onDraw()方法后面的有趣内容。
另外,由于绘制是一种开销很大的操作,因此在相关代码中对效率的优化随处可见,读
者可以留意其改善绘制效率的思想与方式

6.4.1理解 Canvas

      既然要讨论绘制,就不得不提 Canvas。Canvas是一个绘图工具类,其API提供了一系列
绘图指令供开发者使用。根据绘制加速模式的不同, Canvas有软件Canvas硬件Canvas之分。
不过无论软件还是硬件, Canvas的这些绘图指令都可以分为如下两部分:
      口  绘制指令。这些最常用的指令由一系列名为 drawXXX()的方法提供。它们用来实现
            实际的绘制行为,例如绘制点、线、圆以及方块等。

      口  辅助指令。这些指令将会影响后续绘制指令的效果,如设置变换、剪裁区域等。
            Canvas还提供了save()与 restore()用于撤销一部分辅助指令的效果。

既然 Canvas是一个绘制工具类,那么通过它绘制的内容到哪里去了呢?

1. Canvas的绘制目标

软件Canvas来说,其绘制目标是一个建立在 Surface之上的位图 Bitmap,如图6-1所示
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第8张图片

当通过 Surface. lockCanvas()方法获取一个 Canvas时会以
Surface的内存创建一个 Bitmap,通过 Canvas所绘制的内容
都会直接反映到 Surface中。

硬件Canvas绘制目标有两种一种是 HardwareLayer,
读者可以将其理解为一个 GL Texture(即纹理),或者更
简单地认为它是一个硬件加速下的位图( Bitmap)
而另外一种绘制目标则比较特别,被称为
DisplayList。与 Bitmap及 HardwareLayer不同的是, DisplayList不是一块 Buffer,而是一个指
令序列。
DisplayList会将 Canvas的绘制指令编译并优化为硬件绘制指令,并且可以在需要时将
这些指令回放到一个 HardwareLayer上,而不需要重新使用 Canvas进行绘制。

Bitmap、 HardwareLayer以及 DisplayList都可以称为 Canvas的画布。

      从使用角度来说, HardwareLayer与 Bitmap十分相似开发者可以将一个 Bitmap通
过 Canvas绘制到另一个 Bitmap上
,也可以将一个 HardwareLayer绘制到另一个HardwareLayer上。
二者的区别仅在于使用时采用了硬件加速还是软件加速。

另外,将 Displaylist回放到 HardwareLayer上,与绘制一个 Bitmap或 Hardware Layer的
结果开没有什么不同。只不过 DisplayList并不像 Bitmap那样存储了绘制的结果,而
是存储了绘制的过程

理解这三者的统一性对于后续的学习十分重要。

2. Canvas的坐标变换

      相对于绘制指令, Canvas的辅助指令远不那么直观。而且在 Android控件绘制的过程中
大量使用了这些指令,因此有必要先对 Canvas的辅助指令做下了解。其中最常用的辅助指令
莫过于变换指令了。
参考一个例子,当需要在(100,200)的位置绘制一个宽度为50,高度为100的矩形时会
怎么做呢?下面是一种常规的实现
[Sample Code A]
float x = 100f, y = 200f, w = 50f, h = 100f;
mCanvas. drawRect(x, y, x+ w, y + h, mPaint);


这种实现十分直观,按照需求直接在绘制指令中糅合了位置与尺寸参数。
再看另外一种等效的实现方法:
[Sample Code B]
float x = 100f, y = 200f, w = 50f, h = 100f;
//先使用 translate()方法将后续绘制的 坐标系原点  变换  到(x,y)的位置
mCanvas.translate(x, y);
//在新的坐标系下绘制矩形
mCanvas.drawRect(0, 0, w, h, mPaint);

    这种实现通过 Canvas的 translate变换指令剥离了矩形位置信息与尺寸信息,使得
绘制矩形时的参数得到了简化。 translate变换指令改变了后续绘图指令所使用的坐标系
将其在水平方向平移了100个点并在垂直方向平移了200个点,如图6-12所示。随后的
drawRect()便在这个新的坐标系中完成。由于坐标系相对于原始坐标系已经偏移了(100,200),
因此在(0,0)位置绘制的结果与 Sample Code A一致。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第9张图片

      使用变换指令进行绘图看似更加麻烦,而且在一定程度上难于理解。然而试想一下
这里不是绘制一个矩形,而是一连串的图形,使用变换指令可以省却为每一个图形的绘
制指令增加位置参数的麻烦。尤其是
当这些图形由另外的开发者实现时(如
onDraw函数),开发者无须关心任何位
置信息,仅需在原点进行绘制,而位置的
计算则在外部通过一个变换指令一次性完
成。从这个意义上讲,变换指令极大地降
低了复杂绘制的难度。

      另外,在某些绘图需求下,不使用变换指令基本上是无法实现的。例如绘制一个顺时针
旋转60°的矩形时,使用常规方法无法实现,但是却可以通过 Canvas的 rotate指令将坐标
系顺时针旋转90°,然后就可以轻易绘制这个矩形。
除了平移坐标系以及旋转坐标系以外, Canvas还提供了以下变换指令对坐标系进行修改。
scale:对坐标系的刻度进行缩放。例如执行 Canvas. scale(2,3)之后,新坐标系的刻度
将是原坐标系的两倍,即新坐标系
下的(10,30)相当于原坐标系的(20,90)。(坐标被放大)

skew:将坐标系进行切变,其参数
为切变方向角度的tan值。
在经过
这种变换的坐标系中,所绘制的矩
形将会变成菱形,因此这种变换
非常适合做影子效果。
其效果如
图6-13所示。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第10张图片

      当连续进行多次变换时,后一次变换都是在前一次变换后的坐标系中进行的。例如执
行 scale(2,3); translate(2,3)时,新坐标系的原点将会变为原坐标系的(4,9)位置,并且
其刻度大小为原坐标系的2倍。

      当一个 Canvas执行多次变换指令后,要恢复成变换之前的坐标系似乎变得非常困难,因
为必须逐次进行相应的逆变换才行。为了解决这个问题, Canvas提供了配套使用的 save()
restore()方法用以撤销不需要的变换。
参考下面的代码

......//其他代码
//save()将会创建一个保存点
mCanvas.save();
根据需求进行各种变换
mCanvas.translate(,,); mCanvas.scale(.... );  mCanvas.rotate(...);
.......//在新的坐标系下进行绘制
/* restore()方法将会把坐标系恢复到执行save()时的状态,即save()与 restore()之间的坐标变换都会被舍弃*/
mCanvas.restore();

      save()/restore()可以嵌套调用,在这种情况下 restore()将会把坐标系状态返回到与其配对
的save()所创建的保存点。
另外,也可以通过保存某个save()的返回值,并将这个返回值传
递给 restoreToCount()方法的方式来显式地指定一个保存点。

      那么坐标系变换对于控件树的绘制有什么意义呢?开发者在重写View.onDraw()方法时,
从未考虑过控件的位置、旋转或缩放等状态。这说明在 onDraw()方法执行之前,这些状态
都已经以变换的方式设置到 Canvas中了
,因此 onDraw()方法中 Canvas使用的是控件自身
的坐标系。而这个控件自身的坐标系就是通过 Canvas的变换指令从窗口坐标系沿着控件树
步一步变换出来的。当一个父控件进行绘制时,它会首先根据自己的位置、滚动位置以及
缩放旋转等属性对 Canvas进行变换,使得 Canvas的坐标系变换为其自身的坐标系,再调用
onDraw()方法绘制自己。
然后将这个 Canvas交给其第一个子控件,子控件会首先通过 save()
方法将其父控件的坐标系保存下来,将 Canvas变换为自己的坐标系,再通过 onDraw()进行
绘制,然后将 Canvas交给孙控件进行变换与绘制。当子控件及孙控件都完成绘制之后通过
restore()方法将 Canvas恢复为父控件的坐标系,父控件再将 Canvas交给第二个子控件进行
绘制,以此类推。
在后面对控件树绘制的代码分析中将会看到这种递推的变换关系是如何体
现的。

6.4.2 View. invalidate()与脏区域

      为了保证绘制的效率,控件树仅对需要重绘的区域进行绘制。这部分区域称为“脏区域”,
即 Dirty Area。当一个控件的内容发生变化而需要重绘时,它会通过View. invalidate()方法将
其需要重绘的区域沿着控件树提交给 ViewRootlmpl,并保存在 ViewRootlmpl的 mDirty成员
中,最后通过 scheduleTraversals()引发一次“遍历”,进而进行重绘工作。
ⅤiewRootlmpl会保
证仅有位于 mDirty所描述的区域得到重绘,从而避免不必要的开销。

      另外, View.invalidate()在回溯到 ViewRootlmpl的过程中会将沿途的控件标记为脏的
即将 PFLAG_DIRTYPFLAG_DIRTY_OPAQUE两者之一添加到 View.mPrivateFlags成员
中。
两者都表示控件需要随后进行重绘,不过二者在重绘效率上有区别。View有一个方法
isOpaque()供其子类进行重写,用于通过返回值确认此控件是否为“实心”的。所谓的“实
心”控件,是指在其 onDraw()方法中能够保证此控件的所有区域都会被其所绘制的内容完
全覆盖。换句话说,透过此控件所属的区域无法看到此控件之下的内容,也就是既没有半透
明也没有空缺的部分。
在 invalidate()的过程中,如果控件是“实心”的,则会将此控件标记
为 PFLAG_DIRTY_OPAQUE,否则为 PFLAG DIRTY。控件系统在重绘过程中区分这两种标
记以决定是否为此控件绘制背景。
对“实心”控件来说,其背景是被 onDraw的内容完全遮
挡的,因此便可跳过背景的绘制工作从而提高效率。
注意 isOpaque()方法的返回值不是一成
不变的。以 Listview为例,其 isOpaque()方法会根据其 ListItem是否可以铺满其空间来决定
返回值。当 ListItem比较少时它是非“实心”的,而当 Listitem比较多时它则变成“实心”
控件。

      invalidate()方法必须在主线程执行,而 scheduleTraversals()所引发的“遍历”也是在主线
程执行(因为 scheduleTraversalst()是向主线程的 Handler发送消息)。所以调用 invalidate()方
法并不会使得“遍历”立即开始,这是因为在调用 invalidate()的方法执行完毕之前(准确地
说是主线程的 Looper处理完其他消息之前).主线程根本没有机会处理 scheduleTraversals()
所发出的消息。这种机制带来的好处是,在一个方法中可以连续调用多个控件的 invalidate()
方法,而不用担心会由于多次重绘而产生的效率问题。另外,多次调用 invalidate()方法会
使得 ViewRootlmpl多次接收到设置脏区域的请求, ViewRootlmpl会将这些脏区域累加到
mDirty中,进而在随后的“遍历”中一次性地完成所有脏区域的重绘。

      有些时候需要忽略 mDirty的设置以进行完整绘制,例如 窗口的第一次绘制,或者窗口的
尺寸发生变化的时候。
在这些情况下 ViewRootlmpl的 mFullRedrawNeeded成员将被设置为
true,这会使得在绘制之前将 mDirty所描述的区域扩大到整个窗口,进而实现完整重绘

6.4.3 开始绘制

      回到 ViewRootlmpl, performTraversals()的最后一个阶段(即绘制阶段),发现调用了
performDraw()方法,而这个方法就是绘制控件树的入口。
参考其实现
[ViewRootlmpl java-->ViewRootImpl.performDraw()]
private void performDraw(){
    ......
    //①调用draw()方法进行实际的绘制工作
    try t
        draw(fullRedrawNeeded);
    } finally {.......}
    /*②通知WMS绘制已经完成。如前文所述,如果 mReportNextDraw为true,表示WMS正在等侍 
      finishDrawingWindow()回调,以便将窗口的绘制状态切换至COMMIT_DRAW_PENDING
*/
    if (mReportNextDraw) {
        mReportNextDraw = false;
        ......
        try{    
            mWlindowSession.finishDrawing (mWindow);
        } catch ( RemoteException e){}
    }
}
      perform Draw()方法的工作很简单,一是通过draw()方法执行实际的绘制工作,二是如果
需要,则向WMS通知绘制已经完成。注意draw()方法的参数 fullRedrawNeeded来自于前文
所述的成员变量 mFullRedraw Needed
参考 draw()方法的实现:
[ViewRootlmp.java--> ViewRootlmpl.draw()]
private void draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;
    ......
    /*计算mView在垂直方向的滚动量(ScrollY),滚动将保存在mScroller与 mScrollY中, ViewRootImpl
      计算的滚动量的目的与ScrollView或ListView计算的滚动量的意义有别。VisibleInsets存在的
      情况下, ViewRootImpl需要保证某个关键的控件是可见的。例如 当输入法弹出时,接收输入的 TextView
      必须位于不被输入法遮挡的区域内。倘若布局结果使得它被输入法遮挡,就必须根据 VisibleInset
      与它的相对位置计算一个滚动量,使得整个控件树的绘制位置产生偏移从而将 Textview露出来。

      计算所得的滚动量被保存在 mScroller中*/
    scrollToRectOrFocus(null, false);
    int yoff;
    /*上述的滚动量记录在 mScroller中,为的是这个滚动显得不那么突兀, ViewRootImpl使用 mScroller产生
      一个动画效果。 mScroller类似于一个插值器,用于计算本次绘制的时间点所需要使用的滚动量
*/
    boolean animating = mScroller != null && mScroller.computeScrolloffset();
    if (animating) {
        //倘若 mScroller正在执行滚动动画,则采用 mScroller所计算的滚动量
        yoff = mScroller.getCurrY();
    }else {
        //倘若 mScroller的动画已经结東,则直接使用上面的scrollToRectOrFocus()所计算的滚动量
        yoff = mScrollY;
    }
    /*倘若新计算的滚动量与上次绘制的滚动量不同,则必须进行完整重给。
      这很容易理解,因为发生滚动时,整个画面都需要更新
*/
    if (mCurScrollY != yoff) {
        mCurscrollY = yoff;
        fullRedrawNeeded = true;
    }
    ...... // 如果存在一个ResizeBuffer动画,则计算此动画相关的参数
    //如果需要进行完整重绘,则修改脏区域为整个窗口
    if(fullRedrawNeeded) {
        dirty set(0, 0, (int)(mWidth*appScale +0.5f),(int)(mHeight*appScale +0.5f));
    }
    if (!dirty.isEmpty() || mIsAnimating) {
        /*①当满足下列条件时(mAttachInfo.mHardwareRenderer是否存在并且有效),表示此窗口采用硬件加速的绘制方式。
          硬件加速的绘制入口是 HardwareRendere.draw()方法
*/
        if (attachInfo.mHardwareRenderer != null
                && attachInfo.mHardwareRenderer.isEnabled()){
            ......
            if ( attachInfo.mHardwareRenderer.
                    draw
(mView, attachInfo, this, animating? null:mcurrentDirty) ) {......}
        //②而软件绘制的入口则是 drawSoftware()方法
        } else if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
            return;
        }
    }
    // 如果 mScroller仍在动画过程中,则立即安排下一次重绘
    if (animating) {
        mFullRedrawNeeded = true;
        scheduleTraversals();

    }
}
      ViewRootlmpl.draw()方法中产生了硬件加速绘制与软件绘制两个分支,其分支条件为
mAttachInfo.mHardwareRenderer是否存在并且有效。在 ViewRootlmpl.setView()中会调用
enableHardwareAcceleration()方法,倘若窗口的 LayoutParams.flags中包含
FLAG_HARDWARE_ACCELERATED标记,这个方法会通过HardwareRenderer.createGIRenderer()
创建一个HardwareRenderer并保存在 mAttachInfo中。因此 mAttachInfo所保存的 HardwareRenderer是
否存在便成为区分使用硬件加速绘制还是软件绘制的依据。
      硬件加速绘制与软件绘制的流程是一致的,因此接下来将先通过较为简单的软件绘制来了
解控件树绘制的基本流程,然后再以此基本流程为指导来讨论硬件加速绘制所特有的内容。

6.4.4软件绘制的原理

软件绘制由 ViewRootlmpl.drawSoftware()方法完成,参考以下代码:
[ViewRootlmpl.java--> ViewRootlmpl.drawSoftware()]
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff
                                boolean scalingRequired, Rect dirty) {
    ......
    //定义绘制所需的 Canvas
    Canvas canvas;
    try{
        /*①通过 Surtace.lockCanvas()获取一个以此 Surtace为画布的 Canvas。
          注意其参数为前面所计算的脏区域
*/
        canvas = mSurface.lockCanvas(dirty);
    }catch (.....){.....}
    try{
        ......
        /*绘制即将开始之前,首先清空之前所计算的脏区域。这样一来,
          如果在绘制的过程中执行了View.invalidate(),则可以重新计算脏区域
“/
        dirty.setEmpty();
        /*将当前的时间戳保存在 AttachInfo.mDrawingTime中,
          随后控件进行绘制时可以根据这个时间戳以确定动画参数
*/
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        ......
        try{
            /*②使 Canvas进行第一次变换此次变换的目的是使得其坐标系按照之前所计算的滚动量进行相应
              的滚动。Canvas的坐标系变换到控件自身的坐标系下,随后绘制的内容都会在滚动后的新坐标系下进行
*/
            canvas.translate(0, -yoff);
            ......
            //③通过mView.draw()在 Canvas上绘制整个控件树
            mView.draw(canvas);
        } finally {........}
    }finally {
        try {
            //④最后的步骤,通过 Surface.unlockCanvasAndPost()方法显示绘制后的内容
            surface.unlockCanvasAndPost(canvas);
        }catch (IllegalArgumentException e){.......}
    }
    return true;
}
不难看出, drawSoftware主要有4步工作:
口  第一步,通过 Surface.lockCanvas()获取一个用于绘制的 Canvas。
口  第二步,对 Canvas进行变换以实现滚动效果。
口  第三步,通过 mView.draw()将根控件绘制在 Canvas上。
口  第四步,通过 Surface.unlockCanvasAndPost()显示绘制后的内容。

      其中第二步与第三步是控件绘制过程的两个基本阶段,即首先通过 Canvas的变换指令将
Canvas的坐标系变换到控件自身的坐标系下,然后再通过控件的 View.draw( Canvas)方法将控
件的内容绘制在这个变换后的坐标系中。

      注意,在Vew中还有draw( Canvas)方法的另外一个重载,即View.draw( ViewGroup,Canvas, long)。
二者的区别在于后者是在父控件的绘制过程中所调用的(参数 ViewGroup就是其父控件),
并且参数 Canvas所在的坐标系为其父控件的坐标系View.draw( ViewGroup,Canvas,long)
会根据控件的位置、旋转、缩放以及动画对 Canvas进行坐标系的变换,

使得 Canvas的坐标系变换到本控件的坐标系,并且会在变换完成后调用
draw(Canvas)来在变换后的坐标系中进行绘制。
由此看来,相对于另外一个重载,draw( Canvas)
的绘制工作更加纯粹,它用来在不做任何加工的情况下将控件的内容绘制在给定的 Canvas上

这也是为什么将控件内容输出到一个 Bitmap中时使用 draw(Canvas),从而无论控件如何被拉
伸、旋转,目标 Bitmap中存储的都是其最原始的样子。因此draw( Canvas)方法是探讨控件绘
制原理的最佳切入点。
      View. draw(View Group, Canvas,long)的工作远不止坐标系变换那么简单,它还包含
了硬件加速、绘图缓存以及动画计算等工作。但是在讨论它与draw( anvas)之间的
关系时,最重要的还当属坐标系交换。在后面内容的学习中读者会逐步地认识
View.draw(View Group, Canvas, long)

1.纯粹的绘制: View.draw( Canvas)

参考View.draw(Canvas)的实现:
public void draw (Canvas canvas){
    final int privateFlags = mPrivateFlags;
    // 通过检查 PFLAG_DIRTY_OPAQUE是否存在于 mPrivateFlags中以确定是否是“实心”控件
    final boolean dirtyopaque =(privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE
                                &&(mAttachInfo = null || mAttachInfo.mIgnoreDirtystate);
    //①首先是绘制背景。注意,如6.4.2节所述,为了提高效率,“实心”控件的背景绘制工作会被跳过
    if(!dirtyOpaque){
        final Drawable background = mBackground;
        if (background != null){
            //将背景绘制到 Canvas上
            if((scrollX | scrollY) ==0)[
                background.draw(canvas);
            }else{
                /*这里是一个有趣的处理。注意draw( Canvas)方法是在控件自身的坐标系下调用的。就是
                  说 Canvas已经根据其 mScrollX/Y对 Canvas进行了变换以实现控件速动的效果,
                  从而所绘制的背景也会被滚动。不过 Android希望仅滚动件的内容,而保持背景静止。
                  因此在绘制背景时会首先进行滚动的逆变换以撤销先前实行的滚动变换,完成背景绘制之后再将
                  液动变换重新应用到 Canvas上
*/
                canvas.translate(scrollx, scrollY);
                background.draw(canvas);
                canvas.translate(-scrollx, -scrollY);
            }
        }
    }
    //倘若控件不需要绘制渐变边界,则可以进入简便绘制流程
    if (! verticalEdges && ! horizontalEdges){
        //②通过调用onDraw()方法绘制控件自身的内容
        if (!dirtyOpaque) onDraw(canvas);
        /*③通过调用 aispatchDraw()方法绘制子控件。
          如果当前控件不是一个ViewGroup,此方法什么都不做
*/
        dispatchDraw(canvas);
        //④如果有必要,根据滚动状态绘制滚动条
        onDrawScrollBars(canvas);
        //完成此控件的绘制
        return;
    }
    /*接下来是完整绘制流程,完整绘制流程除了包含上面的简便流程之外,还包含绘制渐变边界的工作*/
    .......
}
可见纯粹的控件绘制过程非常简单,主要有以下4步:
      口  绘制背景,注意背景不会受到滚动的影响。
      口  通过调用 onDraw()方法绘制控件自身的内容
      口  通过调用 dispatchDraw()绘制其子控件。
      口  绘制控件的装饰,即滚动条。
除非特殊需要,子控件应当只重载 onDraw()方法而不是draw(Canvas)方法,
以保证背景、子控件和装饰器得以正确绘制

2.确定子控件绘制的顺序: dispatchDraw()

      从分析的轨迹来看,前述的 View.draw()被 ViewRootlmpl.drawSoftware()调用,因此
View.daw()仅仅绘制了根控件自身的内容。那么控件树的其他控件是如何得到重绘的呢?这
将有必要探讨View.dispatchDraw()方法。其在View类中的实现是一个空方法,而 ViewGroup
重写了它。
此方法是重绘工作得以从根控件 mView延续到控件树中每一个子控件的重要纽带。

参考实现如下
[ViewGroup.java--> ViewGroup.dispatchDraw()]
protected void dispatchDraw (Canvas canvas){
    final int count mChildrencount
    final View[] children = mChildren;
    int flags mGroupFlags;
    ...... //动画相关的处理
    int saveCount = 0;
    /*①设置剪裁区域。有时候子控件可能部分或者完全位于 ViewGroup之外。在默认情况下, ViewGroup的下
      列代码通过 Canvas.clipRect()方法将子控件的绘制限制在自身区域之内。超出此区城的绘制内容将会被
      裁剪。是否需要进行越界内容的裁剪取决于ViewGroup.mGroupFlags中是否包含CLIP_TO_PADDING_MASK标记,

      因此开发者可以通过 ViewGroup.setclipToPadding()方法修改这一行为,使得子控件超出的内容仍然得以显示*/
    final boolean clipToPadding = 
                    (flags & CLIP_TO_PADDING_MASK) == CLIP_TO PADDING_MASK;
    if (clipToPadding) {
        //首先保存 Canvas的状态,随后可以通过 Canvas.restore()方法恢复到这个状态
        saveCount= canvas.save();
        // Canvas.clipRert()将保证给定区域之外的绘制都会被裁剪
        canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                        mScrollX + mRight - mLeft - mPaddingRight,
                        mScrollY + mBottom - mTop - mPaddingBottom);

    }
    boolean more = false;
    //获取当前的时间戳,用于子控件计算其动画参数
    final long drawingTime = getDrawingTime();
    /*②遍历绘制所有的子控件。根据GroupFlags中是否存在 FLAG_USE_CHILD_DRAWING_ORDER标记,
      dispatchDraw()会采用两种不同的绘制顺序
*/
    if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0){
        /*在默认情况下, dispatchDraw()会按照 mChildren列表的索引顺序进行绘制
          ViewGroup,addView()方法默认会将子控件加到列表末尾,同时它提供了一个重载,
          允许开发者将子控件派加到列表的一个指定位置。就是说默认情况下的绘制顺席与子控件加入 viewGroup的先后
          或调用 addView()时所指定的位置有关
*/
        for (int i=0; i             final View child children[ili
            if((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                                || child.getAnimation()!= null){
                //③调用 drawchild()方法绘制一个子控件
                more |= drawchild(canvas, child, drawingTime);
            }
        }
    } else{
        /*倘若 mGroupFlags成员中存在 FLAG_USE_CHILD_DRAWING_ORDER标记,则表示此 ViewGroup希
          望按照其自定义的绘制顺序进行绘制。自定义的绘创序由 getChildDrawingOrder()方法实现
*/
        for (int i=0; i             /*与默认绘制次序时的循环变量 i 的意义不同,在这里的 i 并不是指mChildren的索引,而是指已
              经完成绘制的子控件的个数。 getChildDrawingOrder()的实现者可以根据已完成绘制子控件
              的个数决定下一个需要进行绘判的子控件的索引 */

            final View child = children[getchildDrawingOrder(count, i)];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                                    || child.getAnimation() != null){
                //与默认绘顺序一样,通过 drawchild()方法绘制一个子控件
                more |= drawchild(canvas, child, drawingTime);
            }
        }
    }
    ...... //动画相关的处理
    //④通过 canvas.regtoreTocount()撒悄之前所做的剪裁设置
    if(clipToPadding){
        canvas.restoreTocount(saveCount);
    }
    ...... //动画相关的处理
}
      在 dispatchDraw()方法中有很多与动画相关的工作,但它们与绘制的主线流程没有太大关
系,因此本节将它们忽略了,而在6.4.7节中会做详细介绍。
      在本方法中,最重要的莫过于它所定义的两种重绘顺序。重绘顺序对子控件来说意义
非常大,因为当两个或多个子控件有重叠时,后绘制的控件会覆盖先绘制的控件,也就是
说,后绘制的控件拥有被用户看到的优先权。
在默认情况下,后加入 ViewGroup的子控件
位于 mChildren的尾部,因此绘制顺序与加入顺序一致。 ViewGroup可以通过
ViewGroup.setChildrenDrawingOrderEnabled()方法将 FLAG_USE_CHILD_DRAWING_ORDER标记
加入mGroupFlags,并重写 getChildDrawingOrder()来自定义绘制的顺序。

      举例来说, TabWidget维护了一系列的Tab页的标签,而每个Tab页的标签都是它的
个子控件。 TabWidget对绘制顺序的需求是:用户所选择的那个Tab页的标签无论位于
mChildren的什么位置,都希望它能够最后被绘制,以便用户可以不被遮挡地、完整看到它。

默认的绘制顺序无法满足其需求。因此其在初始化时调用了 setChildrenDrawingOrderEnabled(true),
并以如下方式重写了 getChildDrawingOrder()。

[TabWidget.java-->TabWidget.getChildDrawingOrder()]
protected int getChildDrawingorder (int childcount, int i){
    if (mSelectedTab == -1){
        return i;
    }else{
        // 最后一次绘制永远留给被选中的Tab
        if (i == childCount -1)(
            return mSelectedrab
        //而其他的绘制在跳过了选中的Tab之后按照默认的版序进行
        } else if (i >= mSelectedTab){
            return i +1
        } else {
            return i;
        }
    }
}
如此一来,无论发生什么情况都可以保证被选中的Tab会被最后绘制。

      getChildDrawingOrder()决定了绘制的顺序,也就决定了覆盖顺序。覆盖顺序影响了
另一个重要的工作,即触摸事件的派发。按照使用习惯,用户触摸的目标应当是他所能
看到的东西。因此当用户触摸到两个子控件的重叠区城时,覆盖者应当比被覆盖者拥
有更高的事件处理优先级。
因此在6.5.5节讨论触摸事件的派发时将再次看到类似确
定绘制顺序的代码。
      另外, dispatchDraw()在设置裁剪区域的时候把滚动量也考虑在内了。为了解释如此计
算的原因,就必须弄清楚View的 mScrollX/Y两个值的深层含义mScrollX/Y会导致
包括控件自身的内容以及其子控件的位置都产生偏移,这个作用与控件的位置 mLeft/
mTop可以说是一样的。
那么二者的区别是什么呢? mScrolIX/Y描述了控件内容
控件中
的坐标系位置。而 mLeft/mTop则描述了控件本身父控件坐标系中的位置。

控件就好比嵌在墙壁上的一扇窗, mLeft/mTop就相当于这扇窗相对于墙壁左上角的位
置。而控件的内容就像是位于窗后的一幅画,而这幅画相对于窗子的左上角的位置就
是 mScrolIX/Y,
参考图6-14。从这个意义上来讲,当 Canvas针对此控件的 mScrollX/Y
做过变换之后( Canvas.translate(- mScrollX,- mScrollY))的坐标系应该是
[控件内容的坐标系],即以那幅画的左上角为原点的坐标系
。在控件内容的坐标系中,
控件的位置(即那扇窗的位置)是( mScrollX, mScrollY)(如图6-15所示),因此以控件
边界做裁剪时,必须将 mScrolIX/Y纳入计算之列。在不产生歧义的情况下,随后的叙
述中不会区分控件自身的坐标系与控件内容的坐标系。读者只要理解二者之间的差异
在于 mScrollX/Y即可。
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第11张图片

     确定绘制顺序之后,通过 ViewGroup.drawChild()方法绘制子控件。 drawChild()没有什么
额外的工作,仅仅是调用子控件的 View.draw(ViewGroup, Canvas,long)
方法进行子控件的绘制
而已。

3.变换坐标系: View.draw(ViewGroup, Canvas,long)

      在绘制子控件时,父控件调用子控件的 View.draw( View Group, Canvas,long)方法。
6.4.4节开始时曾简单介绍了这个方法的工作内容:为随后调用的 View.draw(Canvas)准备坐
标系。
接下来将详细探讨坐标系的准备过程。另外此方法包含了硬件加速、绘图缓存以及动
画计算等工作
,本节仅讨论软件加速、不使用绘图缓存并且动画计算已经完成的情况下所剩
余的工作,以便能够专注在坐标系变换的原理分析上。参考代码如下:
[View.java-->View.draw(ViewGroup, Canvas, long)]
boolean draw(Canvas canvas, viewGroup parent, long drawingTime) {
    //如果控件处于动画过程中, transtormToApply会存储动画在当前时点所计算出的Transformation
    Transformation transformToApply = null;
    //①进行动画的计算,并将结果存储在 transformmToApply中。这是进行坐标变换的第一个因素
    ......
    /*②计算控件内容的滚动量。计算是通过computeScroll()完成的, computeScroll()将滚动的计算
      结果存储在mScrollX/Y两个成员变量中。在一般情况下,子类在实现 computeScroll()时会考虑
      使用Scroller类以动画的方式进行滚动。向Scroller设置一下目标的滚动量,以及滚动动画的持续
      时同,Scroller会自动计算在动画过程中本次绘制所需的滚动量。
      注意这是进行坐标变换的第二个因素
*/
    int sx=0, sy =0;
    if(! hasDisplayList){
        computeScroll();
        sx = mScrollX;
        sy = mScrollY;
    }
    ......
    /*③使用 Canvas.save()保存 Canvas的当前状态。此时 Canvas的坐标系为父控件的坐标系。在随后
      将Canvas变换到此控件的坐标系并完成绘制后,会通过Canvas.restoreTo()方法将Canvas重置到此时
      的状态,于是 Canvas便可以继续用来给制父控件的下一个子控件了
*/
    int restoreTo = -1;
    if (! useDisplayListProperties || transformToApply != null) {
        restoreTo = canvas.save();
    }
    /*④第一次变换,对应控件位置与滚动量。最先处理的是子控件位置 mLeft/mTop,以及滚动量。
      注意子控件的位置 mLeft/mTop是进行坐标变换的第三个因素
*/
    if (offsetForScroll){
        canvas.translate(mLeft - sx, mTop -sy);
    }
    ........
    /*倘若此控件的动画所计算出的变换存在(即有动画在执行),或者通过View.setScaleX/Y()等
      方法修改了控件自身的变换,则将它们所产生的变换矩阵应用到 Canvas中
*/
    if (transformToApply != null || alpha < 1 || !hasIdentityMatrix() ||
            (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) == PELAG3_VIEW_IS_ANIMATING_ALPHA) {
        if (transformToApply != null || childHasIdentityMatrix) {
            int transx =0, transY =0;
            //记录滚动量
            if (offsetForScroll) {
                transX = -sX;
                transY = -sY;
            }
            //将动画产生的变换矩阵应用到 Canvas
            if (transformToApply != null) {
                if( concatMatrix){
                    if (useDisplayListProperties){
                        /*useDisplayListProperties表示使用硬件加速,由于硬件加速与软件
                          绘制方式上的差异,应用变换炬阵的方式也不同。在讨论硬件加速时再分析这部份
                          内容*/
                        .......
                    } else {
                        /*⑤将动画产生的变换矩阵应用到 Canvas中。
                          注意,这里首先撤销了对滚动量的变换,在将动画的变换矩阵应用给 Canvas之
                          后,重新应用滚动量变换
*/
                        canvas.translate(-transx, -transY);
                        canvas.concat(transformToApply.getMatrix());
                        canvas.translate(transx, transy);
                    }
                }
            ......
            }
            /*⑥将控件自身的变换矩阵应用到 Canvas中。和动画矩阵一样,首先撒销了滚动量的变换,然后
              应用变换矩阵到 Canvas后再重新应用滚动量。控件自身的变换矩阵是进行坐标系变换的
              第四个因素
*/
            if ( !childHasIdentityMatrix && !useDisplayListProperties) {
                canvas.translate(-transx, -transy);
                canvas.concat(getMatrix());
                canvas.translate(transx, transY);
            }
        }
        ......
    } else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET){......}
    /*⑦设置裁剪。当父控件的 mGroupFlags包含FLAG_CLIP_CHILDREN时,子控件在绘制之前必须通过
      canvas.clipRect()方法设置裁剪区域。注意要和 dispatchDraw()中的裁剪工作加以区分。

      dispatchDraw()中的裁剪是为了保证所有的子控件绘制的内容不得越过 ViewGroup的边界。其设置由
      setClipToPadding()方法完成。
而FLAG_CLIP_CHILDREN则表示所有子控件的绘制内容不得超出
      子控件自身的边界,由setClipChildren()方法启用或禁用这一行为。
另外注意,如上一小节所述,
      Canvas此时已经过了 mScrollX/Y的变换,正处在控件内容的坐标系下,因此设置裁剪区域时需要将
      mScrollX/Y计算在内
*/
    if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN 
                                            && !useDisplayListProperties){
        if (offsetForScroll){
            canvas.clipRect(sx, sy, sx + (mRight- mLeft), sy +(mBottom-mTop));
        }else{......}
    }
    .......
    //由于本节讨论的是在不使用绘图缓存情况下的绘制过程,所以 hasNoCache为true
    if( hasnocache){
        boolean layerRendered = false;
        ...... //使用硬件加速绘图缓存的方式对控件进行绘制,本节暂不关注
        if(! layerRendered){
            if(! hasDisplaylist){
                /*⑧使用变换过的 Canvas进行最终绘制。
                  在这里见到了熟悉的dispatchDraw()和draw(Canvas)两个方法。完成坐标的
                  变换之后, Canvas已经位于控件自身的坐标系之下,也就可以通过draw(Canvas)进行
                  控件内容的实际绘制工作
,这样一来,绘制流程便回到了“纯粹的绘制”位置,进而绘制背景、
                  调用onDraw()及 dispatchDraw()再加上绘制滚动条,其中dispatchDraw()还会把绘制
                  工作延续给此控件的所有子控件。

                  注意,当本控件的 mPrivateFlags中包含PFLAG_SKIP_DRAW时,则以dispatchDraw()
                  取代调用draw(canvas)。这是一种效率上的优化。对大多数ViewGroup来说,它们没有
                  自己的内容,即onDraw()的实现为空,在为其设置null作为背景,并且又不要绘制
                  液动条时,其绘制工作便仅剩下 dispatchDraw()了
。对于这种控件,控件系统会为其
                  加上PFLAG_SKIP_DRAW标记,以便在这里直接调用 dispatchDraw()这一捷径从而提高
                  重绘的效率。 PFLAG_SKIP_DRAW标记的设定请参考View.setFlags()*/
                if((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }

            }else {//以硬件加速的方式绘制控件,本节暂不讨论}
        }
    }else if(cache!= null){...... //使用绘图缓存绘制控件}
    /*⑨恢复Canvas的状态到一切开始之前。于是Canvas便回到父控件的坐标系。于是父控件的 
      dispatchDraw()便可以将这个Canvas交给下一个子控件的draw(ViewGroup,Canvas,long)
方法*/
    if (restoreTo >= 0){
        canvas.restoreToCount(restoreTo);
    }
    ......
    //more来自动画计算,倘若动画仍继续,则more为true
    return more;
}
      此方法比较复杂,本节着重讨论两方面的内容,分别是坐标系变换与控件的最终绘制。
      首先是坐标系变换。将 Canvas从父控件的坐标系变换到子控件的坐标系依次需要变换如
下参数:
      口  控件在父控件中的位置,即mLeft/mTop。使用了 Canvas. translate()方法。
      口  控件动画过程中所产生的矩阵在绘制的过程中控件可能正在进行着一个或者多个动
            画,如 ScaleAnimation、 RotateAnimation、 TranslateAnimation等。这些动画根据当前
            的时间点计算出 Transformation,再将其中所包含的变换矩阵通过 Canvas.concact()
            法设置给 Canvas,使得坐标系发生相应变换。
      口  控件自身的变换矩阵。除了动画可以产生矩阵使得控件发生旋转、缩放、位移等效
            果之外,View类还提供了诸如 setScaleX/Y()、 setTranslationX/Y()、 setRotationX/Y()
            等方法使得控件产生上述效果。这一系列方法所设置的变换信息被整合在
            View.mTransformationInfo成员变量中
,并且可以通过View.getMatrix()方法从这个成员变
            量中提取一个整合了所有变换信息的变换矩阵
View.draw( ViewGroup, Canvas,long)
            将这个矩阵 concat()到 Canvas中,
使得这些方法得以产生应用的效果
      口  控件内容的滚动量,即 mScrolIX/Y。虽说在一开始滚动量就和控件位置一起通过
            Canvas. translate进行了变换。然而在进行另外两种矩阵变换时,都会先将滚动量撤销,
            完成变换后再将滚动量重新应用。这说明滚动量是在4种变换因素中最后被应用的。

Canvas针对上述4个因素进行变换之后,其坐标系已经是控件自身坐标系了,接着调用
draw( Canvas)进行控件内容的绘制。
于是便回到了本节所讨论的起点,draw(Canvas)绘制了控
件的背景,通过 onDraw绘制了控件的内容,并且通过它的 dispatchDraw()方法将绘制工作
延伸到属于它的每一个子控件。

4.以软件方式绘制控件树的完整流程

      前三个小节从 View.draw(Canvas),到 ViewGroup.dispatchDraw(),再到子控件的
draw(ViewGroup, Canvas,long),以及子控件的view.draw(Canvas),构成了一个从根控件开
始沿着控件树的递归调用。
于是便可以将控件树绘制的完整流程归纳出来。如图6-16所示
ViewRootlmpl将 mScrollY以 translate变换的方式设置到 Canvas之后, Canvas便位于根控
件的坐标系之中,接下来便通过 View.draw( Canvas)方法绘制根控件的内容。根控件的
dispatchDraw()方法会将绘制工作延续给子控件的view. draw(ViewGroup., Canvas,long),这个方法

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第12张图片

首先以4个变换因素对 Canvas进行坐标系变换,使得 Canvas进入此控件的坐标系,然后
调用View.draw(Canvas)进行绘制,接着此控件的 dispatchDraw()又会将绘制工作延续给子
控件的子控件。
如此一来,绘制工作便沿着控件树传递给每一个控件
      在整个绘制过程中, dispatchDraw()是使得绘制工作得以在父子控件之间延续的纽带
draw(View Group, Canvas,long)是准备坐标系的场所,
draw(Canvas)则是实际绘制的地方。

另外,留意在图6-16所描述的整个绘制流程中,
各个控件都使用了同一个 Canvas,并且它们
的内容通过这个 Canvas直接绘制到了 Surface之上,
图6-17描述了这一特点。在随后讨论硬件加
速与绘图缓存时将会看到与之结构类似但又有所
不同的图。将它们进行对比将有助于深刻理解这
几种不同的绘制方式之间的异同以及优缺点。
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第13张图片

6.4.5硬件加速绘制的原理

      在6.4.3节对 ViewRootlmpl.draw()方法的分析中可以看到,如果 mAttachInfo.mHardwareRenderer
存在并且有效,则会选择使用硬件加速的方式绘制控件树。相对于软件绘制,
硬件加速绘制可充分利用GPU的性能,极大地提高了绘制效率。
      mAttachInfo.mHardwareRenderer存在并有效的条件是:窗口的LayoutParams.flags中包含
FLAG_HARDWARE_ACCELERATED
开发者可以在 AndroidManifest. xml中的 application节
点或 activity节点中声明 hardwareAccelerated属性为tue,使得属于 application或 activity的窗
口获得 FLAG_HARDWARE_ACCELERATED标记,进而启用硬件加速进行绘制
另外,通
过绑定在一个现有窗口的 WindowManager(参考6.2节)所创建的子窗口会自动继承父窗口的
这一标记。


      在 AndroidManifest xm中声明 hardwareAccelerated属性为tue,这会使得从 Package
Manager中所解析出来的 ActivityInfo.flags中包含 FLAG_HARDWARE_ACCELERATED
标记。在初始化对应的 Activity时,会根据从AMS处获取的 Activitylnfo.flag选择是否为
其窗口添加 FLAG_HARDWARE_ACCELERATED标记。
详情请参考 Activity. attach0方
法的实现。
本节将讨论硬件加速绘制的原理以及与软件绘制的异同。

1.硬件加速绘制简介

      倘若窗口使用了硬件加速,则 ViewRootimpl会创建一个 HardwareRenderer并保存在
mAttachInfo
,因此,首先需要理解 HardwareRenderer是什么。顾名思义, HardwareRenderer
是用于硬件加速的渲染器,它封装了硬件加速的图形库,并以 Android与硬件加速图形库的
中间层的身份存在。它负责从 Android的 Surface生成一个 HardwareLayer,供硬件加速图
形库作为绘制的输出目标,并提供了一系列工厂方法用于创建硬件加速绘制过程中所需的
DisplayListHardwareLayerHardwareCanvas等工具。
正如6.3.2节所述, HardwareRenderer
中间层的身份通过它所提供的 updateSurface()、 setup()等(用于向 OpenGL ES提交 Surface的变化)
得到了很好的体现

      注意, HardwareRendererDisplayListHardwareLayerHardwareCanvas等都是抽象类,
它们抽象了在 Android控件系统中进行硬件加速绘制的操作,以便选择在不同硬件加速图形库
的情况下控件系统可以无差别地使用它们。
目前 Android使用 OpenGL ES 2.0进行硬件加速的
绘制,因此上述抽象类的实际实现者分别为GL20RendererGLES20DisplayListGLES20Layer
GLES20Canvas。在这套实现中, Android的 Surface将会被封装为一个 EGLSurface用作输出
目标

      显而易见,倘若需要使用另外一个不同的硬件加速图形库则需要基于这个图形库实现一
整套 HardwareRenderer、 DisplayList、 HardwareLayer以及 HardwareCanvas
。不过目前 Android
中仅有 OpenGL ES这一套实现

      从 HardwareRenderer的继承关系可以体现 Android组织不同硬件加速图形库的方式
HardwareRenderer的直接子类其实是 GLRenderer,而 GLRenderer的子类才是GL20Renderer。
因此第一级的子类( GLRenderer)体现了图形库的种类,而第二级的子类
(GL20Renderer)则体现了图形库的版本。
另外, HardwareCanvas是 Canvas的一个子类,它提供了诸如 drawDisplayList()、
drawHardwareLayer()等硬件加速绘制过程中所特有的操作。而 HardwareCanvas的子类则
将 Canvas类中的操作重定向到了硬件加速图形库中。例如,GLES20Canvas重写了
drawColor(),使得 OpenGL ES 2.0负责完成这个操作,而不是使用Skia软件图形库
在本节后续的内容中读者将会看到这些工具如何在控件绘制中大展身手。

2.硬件加速绘制的入口 HardwareRenderer.draw()

      在6.4.3节中介绍硬件加速绘制与软件绘制分道扬镶的地方是 ViewRootImpl.draw()方法
在这个方法中,硬件加速绘制通过以下代码完成
[ViewRootImpl.java-->ViewRootlmpl.draw()]
private void draw(boolean fullRedrawNeeded){
    if (!dirty.isEmpty() || mIsAnimating){
        if (attachInfo.mHardwareRenderer != null
                && attachInfo.mHardwareRenderer.isEnabled()){
            ......
            //硬件绘制采用的是 HardwareRenderer.draw()方法
            if (attachInfo.mHardwareRenderer.draw(mview, attachInfo, this,
                        animating? null : mCurrentDirty)){........}
        //软件绘制
        }else if (! drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)){
            return;
        }
    }
}
由于 HardwareRenderer是一个抽象类,因此 draw()方法由其子类 GLRenderer实现。
参考如下代码:
[HardwareRenderer.java-->GLRenderer.draw()]
boolean draw (view view, View. AttachInfo attachInfo, HardwareDrawcallbacks callbacks,Rect dirty){
    if( sandra()){
        ......
        //①将 EGLSurface设置为 OpenGL ES当前的输出目标
        final int surfaceState checkCurrent();
        if (surfaceState != SUREACE_STATE_ERROR){
            //②获取对应的 HardwareCanvas
            HardwareCanvas canvas = mCanvas;
            attachInfo.mHardwareCanvas = canvas;
            ......
            int savecount = 0;
            try {
                //如果根控件被 invalidate过,则标记它随后需要刷新其 DisplayList
                view.mRecreateDisplayList = 
                        (view.mPrivateFlags & view.PFLAG_INVALIDATED) == View.PFLAG INVALIDATED;
                ......
                Displaylist displaylist;
                //③获取根控件的 DisplayList
                try{
                    displayList = view.getDisplaylist();
                } finally{......}
                /*onPredraw()方法用于向 HardwareCanvas设置 dirty区域。与软件绘制时通过在
                  包建 Canva时指定dirty区域不同, HardwareCanva是一直存在的,困此需要采取
                  不同的方式设置 dirty区域
*/
                try{
                    status = onPreDraw(dirty);
                } finally {......}
                /*之后的代码将有可能对 Canvas进行变换操作,
                  因此首先保存其状态并在绘制完成后恢复,以便不彩响下次绘制*/
                saveCount = canvas.save();
                /*④callback在绘制前进行一些必要的操作。CallBacks其实是 ViewRootImpl
                  它在onHardwarePreDraw()中将会词用 canvas.translate()以设置Y方向上的滚动
                  量,在软件绘制的 drawsoftware()中也有过这个操作。由于硬件绘制由 
                  HardwareRenderer托管,因此这一操作只能以回调方式完成
*/
                callbacks.onHardwarePreDraw(canvas);
                ......
                if (displayList != null){
                    //⑤绘制根控件的DisplayList
                    try{
                        status |= canvas.drawDisplaylist(displayList, mRedrawClip,
                                                    DisplayList.FLAG_CLIP_CHILDREN);
                    }finally{......}
                }else{......}
            }finally{
                //⑥由 callbacks(即ViewRootImpl)进行绘侧后的工作。
                //  在这里 ViewRootImpl将绘制ResizeBuffer动画

                callbacks.onHardwarePostDraw(canvas);
                //恢复 Canvas到绘制前的状态
                canvas.restoreToCount(saveCount);
                view.mRecreateDisplayList = false;
                ......
            }
            ......
            if ((status & DisplayList.STATUS_DREW) == DisplayList.STATUS_DREW) {
                ......
                //⑦发布绘制的内容。与软件绘制中的Surface.unlockCanvasAndPost()工作一致
                sEgl.eglSwapBuffers(sEqlDisplay, mEglSurface);
                ......
            }
            ......
            return dirty == null;
        }
    }
    return false;
}
以之前所探讨过的 drawSoftware()中的4个主要工作作为对比来看 HardwareRenderer.draw()
方法的实现
      口  获取 Canvas。不同于软件绘制时用 Surface.lockCanvas()新建一个 HardwareCanvas
            在 HardwareRenderer创建之初便已被创建并绑定在由 Surface创建的EGLSurface上。
      口  对 Canvas进行变换以实现滚动效果。由于硬件绘制的过程位于 HardwareRenderer内
            部,因此 ViewRootimpl需要在 onHardwareDraw()回调中完成这个操作。
      口  绘制控件内容。这是硬件加速绘制与软件绘制最根本的区别。软件绘制是通过
            View.draw()以递归的方式将整个控件树用给定的 Canvas直接绘制在 Surface上。而硬件
            加速绘制则先通过 View.getDisplay()获取根控件的DisplayList,然后再将这个
            DisplayList绘制在 Surface上。通过 View. getDisplayList()所获取的 DisplayList中包含
            了已编译过的用于绘制整个控件树的绘图指令。如果说软件绘制是直接绘制,那么硬件
            加速绘制则是通过 DisplayList间接绘制。
获取 DisplayList将是本节所重点讨论的内容。
      口  将绘制结果显示出来。硬件加速绘制通过sEgl.swapBuffers()将绘制内容显示出来。其
            本质与 Surface. unlockCanvasAndPost()方法一致,都是通过 ANativeWindow::queueBuffer()
            将绘制内容发布给 SurfaceFlinger

除了上述4个主要的不同以外,硬件加速绘制还有一个可选的工作,即在
onHardwarePostDraw()回调中由ViewRootlmpl完成 ResizeBuffer的绘制工作。

      关于 ResizeBuffer动画,在6.3.2节介绍的 performTraversals()方法中曾经提到过,
倘若在 performTraversals()方法调用之前(窗口的 resize()回调)或在其调用过程中
( relayoutWindow()方法)使得窗口的 ContentInsets发生了变化, performTraversals()会以
ContentInsets变化前的布局将控件树绘制在一个名为 mResizeBuffer的 HardwareLayer上,
并启动 ResizeBuffer动画( ResizeBuffer动画进行的条件是 mResizeBuffer不为null)。
在 ViewRootImpl.draw()内进行绘制之前,会先根据时间点计算 ResizeBuffer动画的透
明度。如下所示:
if(mResizeBuffer !=null){
    long deltaTime = SystemClock.uptimeMillis() - mResizeBufferStartTime;
    if (deltaTime < mResizeBufferDuration){
        float amt=deltaTime/(float)mResizeBufferDuration;
        amt = mResizenterpolator.getInterpolation(amt);
        animating = true;
        resizeAlpha=255-(int (amt*255);
    } else{
        disposeResizeBuffer();
    }
}
mResizeAlpha = resizeAlpha;
这段代码的意义是在 ResizeBuffer持续的时间内计算一个随时间流逝而递减透明度并
保存在 mResizeAlpha中。而当前时间点超过了 ResizeBufter的持续时间后则通过销毁
mResizeBuffer以结束 ResizeBuffer动画。
而在本节所介绍的 onHardwarePostDraw()回调中, ViewRootlmpl做了如下工作
if (mResizeBuffer !=null){
    mResizePaint.setAlpha(mResizeAlpha);
    canvas.drawHardwareLayer(mResizeBuffer, 0.Of, mHardwareYOfFset, mResizePaint);
}
就是说,在完成控件树的实际绘制之后, ViewRootlmpl在实际绘制的内容之上将
mResizeBuffer (即 ContentInsets)改变前的控件树的快照以动画计算出的透明度绘制
下来。因此当状态栏的可见性发生变化时,用户可以看到一个渐变的动画效果使控件
树以一种布局过渡到另一种布局

3. DisplayList的创建与渲染

      经过前面的分析可以看出硬件加速绘制与软件绘制的根本不同。除了绘制工具以外,便
是硬件加速绘制使用了 DisplayList进行间接绘制。总体来看,硬件加速绘制过程中的View.
getDisplayList()与 HardwareCanvas.drawDisplayList()的组合相当于软件绘制过程中的View.
draw()因此在学习 getDisplayList()时可以与 View.draw()进行类比。
      DisplayList的作用是录制来自 HardwareCanvas的指令,但是使用录制这个词并不是很好
理解。由于它在使用上与一个Bitmap没有太大区别,因此后文在不影响准确性的情况下将称
其录制过程为渲染
参考 getDisplayList()的代码
[View.java->View.getDisplayList()]
public DisplayList getDisplayList(){
    /*通过 getDisplayList()的另外一个重载获取一个渲染过的 DisplayList
      注意第二个参数永远为false*/
    mDisplayList = getDisplayList(mDisplayList, false);
    return mDisplayList;
}
getDisplayList()使用了下面这个重载完成 Display List的创建与渲染
private DisplayList getDisplayList(DisplayLlst displayList, boolean isLayer)

      这个重载接受一个 DisplayList,以及一个布尔型变量作为参数,并返回经过渲染后的DisplayList。
此重载看似多此一举,其实不然。它的无参版本的含义是获取此控件本身经过渲染后
的 Displaylist,而第二个重载则是将控件的内容渲染到任何一个给定的 DisplayList上,
isLayer参数确定给定的 DisplayList是否来自一个绘图缓存。
关于绘图缓存的内容将会
在6.4.6节介绍,读者此时只须了解获取控件本身的 DisplayList时,其 isLayer参数永远为 false即可。
另外,当控件第一次使用硬件加速进行绘制时,其 mDisplayList为null在这种情况,
getDisplayListo的有参重载会创建一个 DisplayList并返回,而其无参重载则会将这个新创建
的 DisplayList保存在 mDisplayList成员变量中。
      接下来看下 getDisplayList()有参版本的实现,这里暂时忽略 isLayer为true以及使用绘图
缓冲的情况
[View java->View.getDisplaylist()]
private DisplayList getDisplayList(DisplayList displaylist, boolean isLayer){
    /*进行 DisplayList的创建是与渲染的条件如下:
      1> mPrivateFlags不存在 PFLAG_DRAWING_CACHE_VALID标记,表示此控件的绘图缓存无效,
           此时需要重新渲染其绘图缓存,本节暂不讨论相关内容
      2> displayList==null,表示此控件是第一次使用硬件加速进行绘制,因此需要创建一个
           DisplayList并对其进行渲染

      3 displaylist.isValid(),当控件从控件树上拿下时,此 displayList会被标记为invalid,
           当其重新回到控件树时,需要对DisplayList进行重新染
      4> mRecreateDisplaylist为true,正如在 GLRederer.draw()中所见,当控件被invalidate之后
           mRecreateDisplayList会被置为true,因此需要进行重新渲染
*/
    if (((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 ||
            displaylist = null || ! displayList isvalid() ||
            (!isLayer && mRecreateDisplayList))){
        ......
        /*①如果参数中没有提供 DiaplayList,则创建一个DisplayList。
          在本例中,这种情况对应着控件第一次使用硬件加速进行绘制
*/
        if (displaylist == null){
            final String name = getClass().getsimpleName();
            /*使用 HardwareRenderer的工厂方法创建一个DisplayList,在目前的版本中它返回的
              Displaylist的类型为GLES20DisplayList*/
            displayList = mAttachInfo.mHardwareRenderer.createDisplayList(name);
            ......
        }
        /*②获取绑定在Displaylist上的Hardwarecanvas。Displalist.start()方法将使
          Displaylist返回一个Hardwarecanvas,并准备好从此 HardwareCanvas中录制绘图操作
*/
        final HardwareCanvas canvas = displayList.start();
        try {
            //设置 Canvas的坐标系为(0,0, width, height)
            canvas.setViewport(width, height);

            /*通过 onPreDraw()设置dirty区域为null,即完整重绘。
              所以DisplayList的渲染是不会考虑dirty区域的
*/
            canvas.onPreDraw(null);
            int layerType = getLayerType();
            if (!isLayer && layerType != LAYER_TYPE_NONE) {
                /*layerType指定了此控件所使用的绘图缓冲的类型,不为 LAYER_TYPE_NONE时表示
                  此控件使用了某种类型的绘图缓冲,本节不讨论此内容
*/
                ......
            }else{
                //③计算并设置控件的滚动量
                computeScroll();
                canvas.translate (-mscrollx, -mscrollY);
                ......
                //接下来的这段代码在View.draw(viewGroup, Canvas,long)中见到过
                if ((mPrivateFlags & PFLAG_SKIP_DRAW)== PFLAG_SKIP_DRAW) {
                    /*跳过本控件的绘制,直接绘制子内容。以此作为无背景
                      ViewGroup绘制时的一条节省开销的快速通道
*/
                    dispatchDraw(canvas);
                } else{
                    //④draw()方法将控件自身的内容绘制到 Hardwarecanvas
                    draw(canvas);
                }
            }
        }finally {
            /*④结束录制。至此为止,通过View.draw()方法对 Hardwarecanva进行的绘制工作都被
              displayList录制并编译优化为一系列的绘图指令。Displaylist的渲染完成,而先前所创
              建的 Hardwarecanva也被回收
*/
            displayList.end();
            ......
        }
    }
    //将渲染过的 DisplayList这回给调用者
    return displayList
}
getDisplayList()的实现体现了 DisplayList的使用方法。 DisplayList渲染与 Surface的绘制
十分相似,分为如下三个步骤

      口  通过 DisplayList. start()创建一个 HardwareCanvas并准备好开始录制绘图指令
      口  使用 HardwareCanvas进行与 Canvas一样的变换与绘制操作
      口  通过 DisplayList.end()完成录制并回收 HardwareCanvas

getDisplay()还体现了另一个重要信息,即 DisplayList的渲染也是使用我们所熟悉的
View.draw()方法完成的,而且 View.draw()方法的实现在硬件加速下与软件绘制下完全一样。

      另外,这一方法还体现了另一个与软件绘制的重要区别:软件绘制的整个过程都使用了
来自 Surface.lockCanvas()的同一个 Canvas;而在硬件加速时,控件使用由自己的 DisplayList
所产生的 Canvas进行绘制。在这种情况下,每个控件 onDraw方法的 Canvas参数各不相同。

还需注意在 getDisplayList()中进行了滚动量的变换,因此在硬件加速绘制的情况下,
View.draw( View Group, Canvas,long)方法不需要进行滚动量的变换
,因而这个方法的流程与软
件绘制时的流程会有差异。

4.硬件加速绘制下的子控件绘制

      DisplayList的实际渲染由 View.draw()完成,可以很自然地联想到硬件加速绘制与软件绘
制进入了统一绘制流程,然而得出这个结论为时尚早。首先回顾一下软件绘制时的总体流程:
      口  View.draw(Canvas)绘制控件本身的内容,并引发 dispatchDraw()的调用以绘制子控件。
      口  ViewGroup.dispatchDraw()根据特定的顺序依次调用子控件的 View.draw( ViewGroup,Canvas, long)。
      口  子控件的 View.draw( View Group, Canvas,long)根据控件的4个坐标系变换因素将
            Canvas坐标系从父控件变换到子控件,然后调用其 View.draw(Canvas)将该坐标系及
            其子控件绘制到 Canvas上。

      硬件加速绘制与软件绘制在前两步是完全相同的。区别在于第三步,即在
View.draw(ViewGroup, Canvas,long)的流程上,二者几乎完全不同。产生不同的根本原因在于硬
件加速绘制希望在 Canvas上绘制子控件的 DisplayList,而不是使用 View.onDraw()直接绘制

这一需求也直接导致了坐标系变换的方法有了不小的差异。本节将讨论此种情况下
View.draw( View Group, Canvas,long)的工作方式。参考如下有关代码
[View. java->View.draw(View Group, Canvas, long)]
boolean draw( Canvas canvas, ViewGroup parent, long drawingrime ){
    //①此方法通过useDisplayListproperties决定是否将变换设置在DisplayList上
    boolean useDisplayListproperties = 
                            mAttachInfo != null && mAttachInfo.mHardwareAccelerated;
    .......
    //② hardwareAccelerated表示 Canvas是否是一个 HardwareCanvas
    final boolean hardwareAccelerated = canvas.isHardwareAccelerated();
    if ((flags & ViewGroup.FLAG_CHILDREN_DRAWN_WITH_CACHE) ! =0 ||
                (flags & ViewGroup.FLAG_ALWAYS_DRAWN_WITH_CACHE)!= 0){
        ......
    } else {
        /*caching表示是否使用缓存。在这里DisplayList也被认为是一种广义上的缓存。
          因此 caching被设置为true
*/
        caching = (layerType != LAYER TYPE NONE)  || hardwareAccelerated;
    }
    ...... //动画计算
    DiplayList displayList = null;
    //④hasDiaplayList表示此控件是否拥有 DisplayList。这取决于 HardwareRenderer是否可用
    boolean hasDiaplayList false;
    if(caching){
        if (! hardwareAccelerated){
            ......//生成软件绘制下的绘图缓存
        }else {
            switch (layerType){
                ...... //硬件加速绘制下,根据绘图缓存类型的不同选择不同的操作
                case LAYER_TYPE_NONE
                    //在 HardwareRenderer可用的情况下, hasDisplaylist为true
                    hasDisplayList = canHaveDisplaylist();
                    break;
            }
        }
    }
    //仅当控件拥有缓存时才可以将变换应用到DisplayList上
    useDisplayListProperties &= hasDisplaylist;
    if (useDisplayListProperties) {
        //⑤通过 getDisplayList()获取本控件经过渲染的DisplayList
        displayList = getDisplayList();
        if ( !displayList.isvalid()){
            //倘若获取DisplayList失败,则在后续的流程中使用软件绘制
            displayList = null ;
            hasDisplayList = false;
            useDisplayListProperties = false;
        }
    }
    .......
    //⑥ hasNoCache表示并非在软件绘制下使用软件绘图缓存。硬件加速模式下它永远为true
    final boolean hasNoCache = cache ==null || hasDisplayList;
    // 倘若有动画正在执行并产生了一个变换矩阵
    if (transformToApply != null || ...... ){
        if (transformToApply != null || !childHasIdentityMatrix) {
            .......
            if (transtormToApply != null) {
                if (concatMatrix){
                    if (useDisplaynistProperties){
                        //⑦将变换矩阵设置到DisplayList中
                        displayList.setAnimationMatrix(transformToApply.getMatrix());
                    }else{
                    ...... // 软件绘制时,在 Canvas中应用动画所产生的变换炬阵
                    }
                }
            }
            ......
        }
        ......
    }
    ..........//设置控件的剪裁区域
    if (hasNoCache){
        boolean layerRendered = false;
        ...... //绘制硬件绘图缓存,倘若没有使用绘图缓存,则layerRendered保持为 false
        if ( ! layerRendered){
            if ( hasDisplayList) {
                ......//软件绘制
            } else {
                //⑧ drawDiaplaylist()将控件的DisplayList绘制在给定的 Canvas上
                ((Hardwarecanvas) canvas).drawDisplayList(displayList, null, flags);
            }
        }
    }else if (cache != null){........}
    ......
    return more;
}
View. draw( ViewGroup, Canvas,long)方法同时兼顾了软件绘制、硬件加速绘制以及使用绘
图缓存等情况
,因此其实现内部的分支非常复杂。如下几个局部变量的取值保证了此方法以
硬件加速绘制的方式执行

      口  useDisplayListProperties,表示应当通过 DisplayList的成员函数来设置坐标系变换,而
            不是像软件加速那样通过 Canvas的变换指令完成

      口  hardwareAccelerated,表示 Canvas是否是一个 HardwareCanvas。
      口  hasDisplayList,表示控件是否可能拥有一个 DisplayList。
            这个值取决于 HardwareRenderer是否可用

      口  caching以及 hasNoCache,是与绘图缓冲相关的两个条件变量。由于 Displaylist被视
            为广义上的一种缓存,因此在硬件加速绘制时 caching为true,而 DisplayList又不属
            于真正意义上的缓存,因此 hasNoCache为 false。

一般情况下,前三个变量的取值是一致的,全部为true则为硬件加速绘制,而全部为
false则为软件绘制。

      对比软件绘制情况下draw( ViewGroup, Canvas,long)的工作,不难发现它使用view.getDisplaylist()
与 HardwareCanvas.drawDisplayList()的组合取代了直接使用 View.draw()。 View.draw()
被 View. getDisplayList()间接调用。
      再看坐标系的变换。由于 useDisplayListProperties为tue,软件绘制时对 Canvas的坐标
系变换全部被绕过了,取而代之的是通过 DisplayList的相关方法进行设置。在6.4.4节曾经
介绍过,从父控件坐标系变换到控件坐标系需要应用4个变换因素,分别为控件位置、动画
矩阵、自身变换矩阵以及滚动量。但是在 View.draw( ViewGroup, Canvas,long)中只看到通过
DisplayList.setAnimationMatrix()设置了动画矩阵,并且在VIew.getDisplayList()的分析中看到
滚动量通过与 DisplayList绑定的 HardwareCanvas.translate()进行了变换
那么另外两个变换
因素是在哪里被应用的呢?
回到 View.get DisplayList()方法的有参重载,可以看到在完成了
View. draw()之后有如下的操作:
private Displaylist getDiplayList(Displaylist displayList, boolean isLayer){
    .......
    try{
        .......
        draw( Canvas)://将控件内容绘制到 DisplayList
    }finaly{
        //重点所在: setDisplayListProperties做了什么?
        setDisplayListProperties (displayList);
    }
}
再看 setDisplayProperties()的代码
void setDisplayListProperties (Displaylist displaylist){
    if (displayList !=null){
        //①设置DisplayList的位置和尺寸。注意位置是控件的位置
        displayList.setLeftTopRightBottom (mLeft, mTop, mRight, mBottom);
    }
    ......
    if (mTransformationInfo != null) {
        /*②设置控件自身的变换信息到DisplayList。控件自身的变换矩阵就是来自于 
          mTransformationInfo,即这种设置和View.getMatrix()是等效的*/

        displayList.setTransformationInfo(alpha,
                            mTransformationInfo.mTranslationX,
                            mTransformationInfo.mTranslationY,
                            mTransformationInfo.mRotation,mTransformationInfo.mRotationx,
                            mTransformationInfo.mRotationy, mTrans formationInfo, mscalex,
                            mTransformationInfo.mScaley);
    }
}
原来不止滚动量,控件的位置以及自身的变换矩阵也实现于 View.getDisplayList()之中
由此总结硬件加速绘制下 View.draw( ViewGroup, Canvas, long)与软件绘制下的不同之处在于:
      变换因素的应用方法不同。软件绘制时通过 Canvas的变换操作将坐标系变换到子
            控件自身坐标系。而硬件加速绘制时 Canvas的坐标系仍保持在父控件的坐标系下,
            然后通过 DisplayList的相关方法将变换因素设置给 DisplayList,
            HardwareCanvas.drawDisplayList()会按照这些变换因素再以这些变换绘制(准确地说是回放) Displaylist

      口  绘制方法不同。软件绘制时可说是直接绘制。硬件加速绘制时使用的是
            View.getDisplayList()与 HardwareCanvas.drawDisplayList()的组合进行间接绘制。

正如如软件绘制的流程一样,View. getDisplayList()中所调用的 View.draw(Canvas)会继续
调用 ViewGroup.dispatchDraw()将绘制工作延伸到下一级的子控件,如此递归下去直到完成整
个控件树的绘制。

5.硬件加速绘制的总结

      相对于软件绘制,硬件加速绘制的流程要复杂一些。其根本原因在于硬件加速绘制是间
接绘制,即在讨论的软件绘制的递归流程之上增加了一部分额外的操作,图6-18体现了硬件
加速绘制在递归方式上的差异。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第14张图片

      View. getDisplayList()这一额外操作使得硬件加速绘制拥有如图6-19所示的特点,其中灰
方块表示一个 Display List对象。
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第15张图片

      从图6-19可以看出,控件内容不再是通过 Canvas直接绘制到 Surface,而是绘制到
DisplayList。同时,子控件的 DisplayList会被绘制到父控件的 DisplayList.。最终整个控件树的
内容被集合在根控件的 DisplayList,并且这个 DisplayList通过 HardwareCanvas绘制到 Surface
之上。

6.4.6使用绘图缓存

      绘制是一个消耗软硬件资源的工作,如果有可能 Android希望能够尽可能地减少绘制工
作,以减少电量的使用以及提高其动画的流畅性。考虑用户拖动一个 Listview的过程中,其
内容每次移动位置都需要 ListView及其列表项进行一次完整绘制。尤其是当列表项是一个复
杂的控件树时,一次完整绘制的开销是很大的。而实际上 ListView的每一个列表项的内容在
拖动过程中往往都是不会发生变化的,变化的仅仅是其位置而已。因此是否可以省略掉列表
项的绘制过程以减少开销呢?绘图缓存就是解决此问题的一个方案。

      所谓绘图缓存是指一个 Bitmap或一个 HardwareLayer,它保存了控件及其子控件的一个
快照。当控件的父控件需要重绘它时,可以考虑将其绘图缓存绘制 Canvas上,而不是执行其
完整的绘制流程,以此节省此控件的绘制开销。
这一机制在控件及其子控件所构成的控件树
十分庞大或者其重绘需要进行复杂计算时尤为有效
      绘图缓存有两种类型,即软件缓存( Bitmap)和硬件缓存( HardwareLayer)。开发者可以
通过 View.setLayerType()为 LAYER_TYPE_SOFTWARE和 LAYER_TYPE_HARDWARE决定
此控件使用哪种类型的缓存。在默认情况下,控件的缓存类型为 LAYER_TYPE_NONE,即不
使用缓存机制。
尽管开发者可以随意设置绘图缓存的类型,但是由于硬件缓存 HardwareLayer
的渲染依赖于 HardwareCanvas,因此在软件绘制的情况下,缓存类型被设置为 LAYER_
TYPE_HARDWARE的控件仍然会选择使用软件缓存。而在硬件加速绘制的情况下,可以在
硬件缓存和软件缓存中任选其一。

      另外,View.setLayerType()除了接受缓存类型作为参数之外,还接受一个 Paint类型的参
数,用于指示控件的绘图缓存将以何种效果绘制在 Canvas上。所支持的Paint的效果有透明
度、 Xfermode以及 ColorFilter。因此绘图缓存除了可以用来提高效率之外,还可以用来实现
一些显示效果。

1.软件绘制下的软件缓存

      首先讨论软件绘制下的软件缓存的工作原理。经过之前关于绘制原理的讨论,不难联想
绘图缓存的相关操作位于 View.draw(ViewGroup, Canvas,long)方法内。参考相关代码:
[View. java-->View.draw(View Group, Canvas, long)]
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime){
    ........
    //①首先获取其缓存类型
    int layerType = getLayerType();
    final boolean hardwareAccelerated = canvas.isHardwareAccelerated();
    if ((flags & ViewGroup.FLAG_CHILDREN_DRAKN_WITH_CACHE) != ||
                (flags & ViewGroup.FLAG_ALWAYS_DRAWN_WITH_CACHE)!=0){
        ...... // ViewGroup会强制子控件使用缓存进行绘制,本节最后再讨论这个话题
    }else{
        //②如果缓存类型不为NONE则表示启用缓存
        caching= (layerType != LAYER_TYPE_NONE) || hardwareAccelerated;
    }
    .......//动画处理
    // cache是一个 Bitmap类型的变量,用于保存软件缓存
    Bitmap cache = null;
    if (caching) {
        //软件绘制时的存处理
        if (! hardwareAccelerated) {
            if (layerType != LAYER_TYPE_NONE){
                //软件绘制情况下不支持硬件缓存,因此将强制使用软件缓存
                layerType= LAYER_TYPE_SOFTWARE;
                //③buildDrawingCache()将在必要时刷新缓存的内容
                buildDrawingcache(true);

            }
            //④通过 getDrawingCache()获取控件的软件缓存,并保存在 cache中
            cache = getDrawingcache (true);
        }else{
            ......//硬件加速下的缓存处理
        }
    }
    ......
    /*接下来这段代码需要留意,在使用绘图缓存时不会进行滚动量的变换。这是为什么呢?
      因为滚动量的变换操作在生成绘图缓存时便已经完成了。绘图缓存作为控件内容的快照,应该如实地反映
      控件当前的模样,例如一个 Listview的快照应当能够反映其滚动的位置。因此在这里便不可以再对
      滚动量做变换了*/
    final boolean offsetForScroll = cache == null && !hasDisplayList &&
                                layerType != LAYER_TYPE_HARDNARE;
    if (offsetForScroll) {
        canvas.translate(mLeft- sx, mTop - sy);
    } else {
        if (! useDisplayListProperties) {
            canvas.translate(mleft, mTop);
        }
    }
    ......
    final boolean hasNocache = cache == null || hasDisplayList;
    ...... //冗长的坐标系变换操作
    if (hasNoCache){
        ...... //硬件缓存操作与无缓存绘制的代码
    } else if (cache != null) {
        .......
        /*⑤将软件缓存 cache绘制到 Canvas上。其中 cacePaint保存了
          View.setLayoutType()时所传入的参数
*/
        canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
    }
    .......
    return more;
}
      显而易见,使用软件缓存进行绘制时使用 View.buildDrawingCache()/getDrawingCache()与
canvas.drawBitmap()的组合替代无缓存模式下的 View. draw(Canvas)。此种模式是否有些似曾
相识的感觉呢?猜对了,和硬件加速绘制时的处理如出一辙。类比地推测一下,在负责刷新
缓存的 View. buildDrawingCache()中一定包含了对 View.draw(Canvas)的调用。而且事实的确
如此。参考Vicw. buildDrawingCache()的代码:
IView.java--> View.buildDrawingCache()]
public void buildDrawingCache(boolean autoScale){
    /*仅当软件缓存为invalidate(伴随着View的 invalidate)时或控件尚未生成存时,
      此方法才会执行。
倘若在控件没有被invalidate时仍生成软件缓存,那便失去了缓存的意义*/


    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 
            || (autoScale ?mDrawingCache == null : mUnscaledDrawingCache == null)){
        ......
        /*软件缓存有两块,区别在于mDrawingCache会根据兼容模式进行放大和缩小,
          而mUnscaledDrawingCache则反映了控件的真实尺寸,这两者的用途是不一样的。
          mDrawingCache用于做绘制时的软件缓存,因为绘制到窗口时需要根据兼容模式进行缩放。
          而mUnscaleDrawingCache则往往被用作控件截图等用途。

          注意到View.draw(ViewGroup, canvas,long)调用buildDrawingcache()时
          autoScale参数为true,所以mDrawingCache才是本节所讨论的那个绘图缓存*/
        Bitmap bitmap = autoScale ? mDrawingcache : mUnscaledDrawingCache;
        //若缓存不存在,或者控件的最新尺寸与缓存尺寸不一致则需要新建一块缓存
        if (bitmap == null ||
                bitmap.getWidth() != width || bitmap.getHeight()!= height){
            ......
            if (bitmap != null) bitmap.recycle();
            try{
                //①新建一个缓存。可见软件缓存是一个不折不扣的普通 Bitmap
                bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),width, height, quality);
                bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
                //保存 Bitmap到合适的成员变量中
                if (autoscale) {
                    mDrawingCache = bitmap;
                }else {
                    mUnscaledDrawingCache = bitmap;
                }
            } catch (OutotMemoryError e) {......}
        }
        //确保缓存已经创建之后,就要准备 Canvas了
        Canvas canvas;
        if (attachInfo != null){
            /*Canvas使用了mAttachinfo中的mCanvas。AttachInfo.mCanvas可说是一个缓存。所有
              需要刷新软件缓存的控件都可以从这里取出这个已创建好的Canvas进行绘制,以避免
              每个控件在每次新缓存时创建和销级 Canvan所带来的开销
*/
            canvas = attachInto.mCanvas;
            if(canvas = null) {
                canvas = new Canvas();
            }
            //②设置软件缓存为 Canvas的绘制目标
            canvas.setBitmap (bitmap);
            /*这是一个有趣却重要的小技巧。 AttachInfo.mCanvas是一个公共的缓存。那么控件树中任何一个
              需要刷新软件缓存的控件都会到这个成员,而且都会通过上述 setBitmap()调用将其绘制目标
              设置为各自的软件缓存。
设想一下,如果此控件的某个子控件也使用软件缓存,那么这个 Carvas
              的绘制目标会被这个子控件篡改,结果将是灾难性的。因此在这里将其设置为null,使其子控件
              不得不创建自己的 Canvas。不过兄弟控件之间共用这一个 Canvas是没有问题的,因为它们
              的绘制是串行的
*/
            attachInfo.mCanvas = null;
        }else{......}
        
        //③计算滚动量并对坐标系进行滚动量变换
        computeScroll();

        ......
        canvas.translate(-mScrollX, -mScrollY);
        .......
        /*④ draw(Canvas)或是 dispatchDraw(),这已是第三次见到这一熟悉的操作了。
          这将会把控件及其子控件的内容绘制在 Bitmap,即软件缓存之上*/
        if ((mPrivateElags & PFLAG_SKIP_DRAW)== PFLAG_SKIP_DRAW) {
            mPrivateFlags &= -PFLAG_DIRTY_MASK;
            dispatchDraw(canvas);
        }else{
            draw(canvas);
        }
        //将 Canvas放回 AttachInfo.mCanvas中,于是其他控件又可以使用这个 Canvas了
        if (attachInto != null){
            attachInfo.mCanvas = canvas;
        }
    }
}
      果不其然, View.buildDrawingCache()的实现方式与View.getDisplayList()方法几乎完全一
致。只不过它的目标是一个Bitmap而不是 DisplayList。

      而Ⅴiew.getDrawingCache()则返回 mDrawingCache或 mUnscaledDrawingCache。这个方法
虽然简单,但仍然有值得讨论的地方。参考实现如下
public Bitmap getDrawingcache(boolean autoscale){
    /*① WILL_NOT_CACHE_DRAWING 标记表示此控件不使用软件缓存。某些控件会添加这个标记,因为它们
      有意无意地已经实现类似绘图缓存的机制。例如 ImageView,它的绘制本身就是绘制一个图片,将其绘制
      到软件缓存,再将其软件缓存绘制到 Canvas上,这明显不如直接绘制到 Canvas上来得快。

      参考View.setWillNotCacheDrawing()*/
    if ((mviewFlags & WILL_NOT_CACHE_DRAWING)== WILL_NOT_CACHE_DRAWING) {
        return null;
    }
    /*② DRAWING_CACHE_ENABLED 表示了控件使用软件缓存。这岂不是和WILL_NOT_CACHE_DRAWING重复?
      二者作用类似,但是意图却不一样。 WILL_NOT_CACHE_DRAWING表示控件根本不希望使用软件缓存,
      即便通过 setLayerType()设置了一种缓存类型也不行,控件本身的性质决定了是否添加 WILL_NOT_
      CACHE_DRANING标记,其意图在于禁用软件缓存。
DRANING_CACHE_ENABLED标记的意图则在于启用
      软件缓存,但不是在用在绘制过程中,而是用于通过 getDrawingcache()进行控件截图时使用的。

      即便不存在这一标记,只要通过 setLayerType()设置了一种绶存类型,在绘制过程中依然会采用绘图
      缓存的方式进行绘制

      另外, ViewGroup也会通过这个方法临时启用其直接子控件的软件缓存。在本节的最后将会讨论这一话题*/
    if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {
        buildDrawingcache(autoscale);
    }
    //返回 mDrawingcache或 mUnscaledDrawingcache
    return autoscale? mDrawingCache : mUnscaledDrawingCache;
}
      只要理解了硬件加速绘制的原理,那么理解软件绘制下的软件缓存的工作原理并不困
难。图6-18以及图6-19完全适用于描述软件绘制下的软件缓存的流程特点。只需要将
View.getDisplayList()换作 View.buildDrawingCache(),以及将 DisplayList换作 Bitmap即可。

2.硬件加速绘制下的绘图缓存

      接下来讨论硬件加速下的绘图缓存的实现原理。硬件加速绘制时,绘图缓存的实现位于
View.getDisplayList() 
而不是 View.draw(ViewGroup,Canvas,long)中。如果将 DisplayList理解为
一种缓存,那么硬件加速绘制下的绘图缓存则是在 DisplayList的基础之上的另外一级缓存
,
即二级绘图缓存。参考Vicw. getDisplayList()中的相关代码,注意此时第二个参数 isLayer仍
然为 false
[View. java--> View.getDisplayList()]
private Displaylist getDisplayList (Displaylist displayList, boolean isLayer){
    ......
    if (((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID)==0 ||
            displayList == null || !displayList.isValid() ||
            (! isLayer && mRecreateDisplaylist))){
        ...... //创建 DisplayList
        boolean caching = false;
        final HardwareCanvas canvas = diaplayList.start();
        try{
            ......
            // ①获取缓存类型
            int layerType = getLayerType();
            if (!isLayer && layerType != LAYER_TYPE_NONE){
                // 处理硬件缓存
                if(layerType == LAYER_TYPE_HARDWARE){
                    //②通过 getHardwarelayer()方法获取刷新后的硬件缓存
                    final HardwareLayer layer = getHardwareLayer();
                    if (layer != null && layer.isValid()){
                        /*③通过 Hardwarecanvaa.drawHardwareLayer()方法
                          将硬件缓存绘制到 DisplayList上
*/
                        canvas.drawHardwareLayer (layer, 0, 0, mLayerPaint);
                    }else {......}
                //处理软件缓存
                } else {
                    // ④软件缓存的处理方法与软件绘制时的件缓存完全一致
                    buildDrawingcache(true);
                    Bitmap cache= getDrawingcache (true);
                    if(cache!=null){
                        canvas.drawBitmap (cache, D, 0, mLayerPaint);
                    }
                }
            }else{
                ...... //在无缓存绘制的情况下,调用View.draw(Canvas)对 Displaylist进行渲染
            }
        } finally {.......}
    }
    return displayList;
}
      首先,软件缓存的处理方式与软件绘制时完全一致,因此不必赘述。仅需注意硬件加速
时的软件缓存被绘制在 DisplayList上即可。

      硬件加速绘制的判断条件是 AttachInfo.mHardwareRenderer不为空并且有效。这说明
硬件加速特性的启用与否是窗口级的,即对整个控件树有效,好似整个控件树中的
控件都会以硬件加速的方式进行绘制。其实不然,软件缓存的存在会导致控件树的
某个子树退化为软件绘制。因为当一个控件在硬件加速绘制的情况下启用软件缓存
时,它的 View.draw( canvas)方法将会在View.buildCachet()中调用,并使用
AttachInfo.mCanvas进行绘制。这导致这一控件及其子控件都得使用软件 Canvas进行
绘制。

这一现象更体现了硬件加速绘制的复杂性,因为在一棵控件树中可能同时存在着多种
绘制方式:标准的硬件加速绘制、软件绘制、软件缓存绘制以及硬件缓存绘制。

      而硬件缓存的处理则引人了一个新的方法:View.getHardwareLayer() HardwareLayer
我们所说的硬件缓存。它的作用与 View.buildDrawingCache()/getDrawingCache()的组合是
一致的,即刷新并获取绘图缓存。
于是可以预料到 View.getHardwareLayer()的实现应该与
buildDrawingCache()十分类似。参考以下实现
HardwareLayer getHardwarelayer(){
    ...... //检查硬件加速是否可用
    final int width= mRight -mLeft;
    final int height = mBottom -mTop;
    ......
    //此方法的执行条件与View.buildDrawingcache的相同
    if ((mPrivateFlags & PFLAG DRAWING CACHE VALID) ==0 || mHardwareLayer == null){
        if (mHardwareLayer null){
            //①如果控件尚无硬件缓存,则通过 HardwareRenderer创建一个
            mHardwareLayer = mAttachinfo.mHardwareRenderer.createHardwareLayer(width, height, isOpaque());
            ......
        } else{
            //②当尺寸发生变化时,与软件缓存时 必须重新创建,不同硬件缓存可以直接修改尺寸
            if (mHardwareLayer.getWidth() != width
                || mHardwareLayer.getHeight()!= height){
                if (mHardwareLayer.resize(width, height)){
                    mLocalDirtyRect.set(0,0, width, height);
                }
            }
            ......
        }
        //设置将硬件缓存绘制到Canvas上时所使用的paint
        mHardwareLayer.setLayerPaint(mLayerPaint);
        //③设置用来刷新硬件缓存的DisplayList。这一DisplayList
          由View.getHardwarelayerDisplayList()方法获得
*/
        mHardwarelayer.redrawLater(getHardwareLayerDisplaylist(mHardwareLayer),mLocalDirtyRect);
        ViewRootImpl viewRoot = getViewRootImpl();
        //由 ViewRootImpl通知HardwareRenderer尽快使用给定的 Displaylist刷新硬件缓存
        if (viewRoot != null) viewRoot.pushHardwareLayerUpdate(mHardwareLayer);
        ......
    }
    return mHardwarelayer;
}
其实现与预想之中似乎有一些不同,它并没有直接调用 view.draw(Canvas)进行绘
制,而是使用 View.getHardwareLayerDisplayList()方法获取了一个 DisplayList,并以这
DisplayList对 HardwareLayer进行刷新。这究竟是一个什么样的 Display List呢?参考get
Hardware LayerDisplayList()方法的实现:
[View.java-->View.getHardwareLayerDisplayList()]
private DisplayList getHardwareLayerDisplayList(BardwareLayer layer) {
    /*又一次回到View.getDisplaylist(),注意这一次 getDisplayList()的参数与之前的不同。传入
      的Displaylist来自给定的 Hardwarelayer,而不是控件自身的 mDisplaylist,并且第二个参数
      layer的值为true,而不是以往的fase
*/
    DisplayList displayList = getDisplayList(layer.getDisplaylist(), true);
    layer.setDisplayList(displaylist);
    return displayList;
}
看来View.getHardwareLayerDisplayList()所返回的 DisplayList也来自 View.getDisplayList(),
只不过采用了不同的参数而已。可以断定此 DisplayList中记录的就是控件的绘制结果,不过
isLayer参数会为tue带来什么样的效果呢?再回过头来看view.getDisplayList()的实现,注意
这一次 isLayer为true
[View.java-->View.getDisplayList()]
private Displaylist getDisplayList(DisplayList displaylist, boolean isLayer){
    .......
    if (((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 ||
                    displayList == null || !displayList.Isvalid()||
                    (!isLayer && mRecreateDisplayList))){
        ...... //创建DisplayList
        boolean caching = false;
        final HardwareCanvas canvas = displayList.start();
        try{
            ......
            //①获取存类型,注意这时类型为硬件缓存
            int layerType = getLayerType();
            //②这次isLayer为true,即不会进行硬件缓存的绘制
            if (!isLayer && layerType != LAYER_TYPE_NONE){
                .....//硬件缓存或软件缓存的绘制
            }else{
                //③通过View.draw(Canvas)渲染 DisplayList
                .......//无缓存绘制的情况下调用View.draw(Canvas)对 DisplayList进行渲染
            }
        } finally {......}
    }
    return displayList;
}
原来, isLayer 最大的意图就是为了区分 getDisplayList()的结果是用于实际绘制,还是
于刷新硬件缓存
。如果是实际绘制( isLayer 为false),则会根据 getLayerType()的返回值确定
采用硬件缓存、软件缓存或 View.draw( Canvas)之一对 Display List进行渲染。
如果是用于刷新
硬件缓存
( isLayer 为true),则仅使用 View.draw( Canvas)进行渲染。
因此在启用硬件缓存之
后, View.getDisplayList()会被调用两次,第一次调用是为了渲染控件自身的 DisplayList,其渲
染内容为绘制一个硬件缓存,即 HardwareLayer。而第二次调用则是为了渲染 HardwareLayer
的 DisplayList,其渲染内容是 View.draw( Canvas)方法。

      总结硬件加速下的绘图缓存,无论硬件缓存还是软件缓存都可以得到比图6-19更复杂的
图6-20,其中斜纹方框为绘图缓存,而灰色方框则是 DisplayList
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第16张图片

3.绘图缓存的利与弊
      绘图缓存的目的在于减轻控件的绘制负担,但是对比图6-17、图6-19以及图6-20,可以
很明显地发现绘图缓存的引入增加了将绘图缓存绘制到实际目标( Surface或 DisplayList)的
操作,这个相对于正常绘制的额外动作,反而增加了绘制过程的开销。因此倘若不警慎地使
用绘图缓存反而会使绘制性能下降。因此有必要讨论一下究竞应该如何能够在开发过程中充
分利用绘图缓存的优势,并将其带来的额外开销降到最低。

参考图6-21所标示的控件树
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第17张图片

假设View执行了 invalidate()操作而导致控件树重绘。在
没有启用绘图缓存的情况下,则会使得并没有发生内容变化的
ViewGroup 1、 View 1-1以及View1-2发生重绘。
而倘若 ViewGroup 1
启用了绘图缓存,则会通过将 ViewGroup的缓存绘制到 Canvas从
而省略三个控件的重绘过程。
此时绘图缓存会带来性能的提升,前
提是 ViewGroup 1及其子控件的绘制工作的负荷 大于 绘制一个 Bitmap
或 HardwareLayer。
反之,如果View 1-1或View1-2的构成十分简单
绘制动作开销也不大(例如它们是个 TextView或者一个简单色块)
那么绘制缓存的开销反而比重绘要大,使用绘图缓存就得不偿失了。

      再考虑另外一种情况,图6-21所述的 ViewGroup 1是一个 ListView。当发生拖动操作
时,倘若没有为View1-1或View1-2启用绘图缓存,会使得每一次改变它们的拖动位置时产
生重绘操作。如果它们是包含了很多控件的控件树,其拖动效率会很低。在为View1-1以
及View1-2启用绘图缓存后,会省略它们的重绘而代之以绘制其缓存,从而使拖动效率得到
提升。

      仍然是图6-21所述的控件树,在 ViewGroup 1启用绘图缓存的情况下, View 1-1执行了
invalidate()操作而导致控件树重绘,这会使得 ViewGroup 1的绘图缓存无效而进行绘图缓存的
更新,因此 ViewGroup 1、View1-1以及View 1-2产生重绘操作,使得绘图缓存得到更新以反
映View1-1的最新内容。进一步,更新后的绘图缓存被绘制到 RootView所给予的 Canvas上。

相对于没有启用绘图缓存的情况,绘制绘图缓存的动作变成了额外的负担,因此绘图缓存的
存在降低了绘图的效率
从上述几个情况的讨论可以总结出使用绘图缓存的原则:
      口  首要原则是不要为十分轻量级的控件启用绘图缓存。因为缓存绘制的开销可能大于此
            控件的重绘开销。

      口  为很少发生内容改变的控件启用绘图缓存。因为启用了绘图缓存的控件在 invalidate()
            时会产生额外的缓存绘制操作。

      口  当父控件要频繁改变子控件的位置或变换时,对其子控件启用绘图缓存。这会避免频繁
            地重绘子控件。

针对第三个原则, ViewGroup提供了 setAlwaysDrawnWithCacheEnabled()以及
setChildrenDrawnWithCacheEnablede()
两个方法,用于一次性为所有子控件启用绘制缓存。当通过
setAlwaysDrawnWithCacheEnabled()FLAG_ALWAYS_RAWN_WITH_CACHE标记置入
mGroupFlags后,将总是使用缓存的方式绘制子控件。
setChildrenDrawnWithCacheEnabled()
则允许根据需求启用或禁用以缓存的方式绘制子控件。
正如在 View.draw(View Group, Canvas,long)
方法中所看到的,当 FLAG_ALWAYS_DRAWN_WITH_CACHE或 FLAG_CHILDREN_
DRAWN_WITH_CACHE标记存在时,局部变量 cache被置为true,无论View的 layerType被
设置为何种值。 AbsList view通过这个方法以提高其拖动时的性能。
有兴趣的读者可以参考其
实现。

6.4.7控件动画

      控件系统中存在三种方式实现控件的动画其一是使用 ValueAnimator类或其子类
ObjectAnimator,或者 ViewPropertyAnimator类周期性地改变控件的某个属性从而达到动画的
效果。其二是使用 LayoutTransition类,用于在 ViewGroup中删除或者添加一个控件时对其应
用一个进入或移出的动画, LayoutTransition类使用了 ObjectAnimator类实现其动画,当子控件
被添加或删除时, ViewGroup会根据 LayoutTransition中的设置启动对应的动画。其三是使用
View.startAnimation()方法启动一个动画。

      使用 ValueAnimator、 ObjectAnimator进行控件动画的原理与绘制的内部过程并没
有十分紧密的联系。ValueAnimator内部有一个实现了 Runnable接口的、线程唯一的
AnimationHandler类。当动画运行时, ValueAnimator会将这个 AnimationHandler不断地抛
给 Choreographer,并在SYNC事件到来时修改指定的控件属性,控件属性的变化引发
invalidate()操作进而进行重绘,以此实现动画效果。
虽然 ViewPropertyAnimator与
ValueAnimator没有继承关系,但它其实是 ValueAnimator的一个封装,通过其实现动画效果。
而 View.startAnimation()的动画与控件绘制的内部过程联系十分紧密。因为在控件的坐标
系变换中,动画矩阵是变换因素之一。相信读者对于控件的坐标系变换已经十分熟悉,本节
将在此基础之上讨论其 View.startAnimation()动画的实现原理。

1.启动动画

参考Vew. startAnimation()的实现:
[View java-->View.startAnimation()]
public void startAnimation(Animation animation) {
    ......
    //保存 Animation对象
    setAnimacion(animation);
    ......
    //invalidate()操作,以此触发一次重绘
    invalidate (true);

}
public void setAnimation(Animation animation){
    mCutrentAnimation = animation;
    ......
}
可以看出, startAnimation()方法启动的动画依托于 Animation类的子类。启动动画时首先
将给定的 Animation通过 setAnimation()保存到 mCurrentAnimation成员中,再通过 invalidate()
方法触发一次重绘。
接下来就看重绘时如何使用这个动画了。

2.计算动画变换

既然动画是以坐标系变换的方式产生效果的,因此
进行动画计算的代码位于View.draw(ViewGroup, Canvas, long)方法中。
[View. java--> View.draw(ViewGroup, Canvas, long)]
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ......
    //①获取 startAnimation()所给予的 Animation对象
    final Animation a = getAnimation();
    if(a!=null){
        /*②通过 drawAnimation()方法计算当前时间点的变换(Transformation)。
          结果保存在parent.mChildTransformation中
*/
        more= drawAnimation (parent, drawingTime, a, scalingRequired);
        transformToApply = parent.mChildTransformation;
    }else{........}
    //随后的过程在介绍绘制原理时已经说明过了,即把 transformToApply应用到坐标系变换中
    .......
}
接下来参考 drawAnimation()方法的实现
[View.java-->View.drawAnimation()]
private boolean drawAnimation(ViewGroup parent, long drawingTime,Animation a, boolean scalingRequired){
    .......
    final boolean initialized = a.isInitialized();
    /*①首先检查 Animation是否初始化。当尚未初始化时,表明这是动画的第一帧。
      此时View.onAnimationStart()回调将会被调用
*/
    if (!initialized){
        /*介绍WMS窗口动画时曾经介绍过 Animation.initialize()的作用。
          当动画使用了相对参数时此处传入的矩形区城将作为参考*/
        a.initialize(mRight -mLeft, mBottom -mTop, parent.getwidth(), parent.getHeight());
        ......
        /*注意, onAnimationStart的默认实现会将 PFLAG_ANTMATTON_STARTED标记加入
          View.mPrivateFlags中,用以标记控件正在运行动画。
因此重写 onAnimationStart()时一定要调用基类
          的 onAnimationStart()方法以保证控件系统的正常工作
*/
        onAnimationstart();
    }
    //②计算当前时刻的变换。变换被保存在父控件的mChildTransformation成员中
    boolean more = a.getTransformation(drawingTime, parent.mChildTransformation, 1f);
    /*倘若动画继续进行,则需要再次进行invalidate()以便进行下一帧的计算与绘制。为了提高效率需要
      指定 invalidate()的边界以便在下次绘制时仅更新此控件的区城。正常来说,只要调用 parent.invalidate
      ( mLeft,mrop, mRight, mBottom)即可。但是因为动画可能会改变控件的绘制位置(如 ScaleAnimation,
       TranslateAnimation等),因此mLeft、mTop、 mRight以及 mBottom已经不能用来表示
      控件在父控件中的最终位置了。因此对上述4个参数进行同样的动画变换以正确表示最终位置。
      为 此drawAnimation()使用一个局部变量 invalidationTransformation
      用以表示进行 invalidate()时需要进行的变换
*/
    if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
        /*在兼容模式下,控件都被进行了缩放,这个缩放同样会影响invalidate()的区域。
          这一缩放所产生的变换保存在 ViewGroup.mInvalidationTransformation
*/
        if (parent.mInvalidationTransformation = null){
            parent.mInvalidationTransformation = new Transformation();
        }
        invalidatfonTransform = parent.mInvalidationTransformation;
        //将动画的变换附加到兼容模式的变换之后
        a.getTransformation(drawingTime, invalidationTransform, lf);
    } else{
       / /非兼容模式下, invalidate参数所需的变换与动画变换相同
        invalidationTransform = parent.mChildTransformation;
    }
    //③如果动画还将继续,则通过调用父控件的invalidate()触发下一次的重绘
    if (more){
        if ( !a.willChangeBounds()){
            //如果动画不改变控件的边界(如AlphaAnimation),则按照控件正常的区域进行invalidate
            .......
        } else {
            //使用invalidateTransformation对 invalidate()的4个参数进行变换
            final RectF region = parent.mInvalidateRegion;
            a.getInvalidateRegion(0, 0, mRight - mLeft, mBot tom -mTop, region,
                                    invalidationTransform);
            .......
            // 对4个参数进行修正,并进行 invalidate()
            final int left = mLeft +(int) region.left;
            final int top = mTop+ (int) region.top;
            parent.invalidate(left, top, left + (int)(region.width()+ .5f),
                            top +(int)(region.height()+.5f));
        }
    }
    return more;
}
在学习WMS窗口动画的原理后,不难理解这个过程。 drawAnimation()通过 Animation.
getTransformation()计算当前时间点的变换,并将其保存在父控件的 mChildTransformation成
员中,然后在 View.draw( View Group, Canvas,long)方法中将这个变换以坐标系变换的方式应用
到 Canas或者 DisplayList中,从而对最终的绘制结果产生影响。倘若动画还将继续,则调用
invalidate()以便在下次SYNC事件到来时进行下一帧的计算与绘制。
      本来,这个代码可以很简单,即 Animation.getTransformation()+ View.invalidate()即可,但
是正如在控件系统其他方面的工作中所见到的,效率永远是控件系统需要考虑的第一位因素
因此 drawAnimation()才不惜篇幅地计算 invalidate区域。

3.动画的结束

  倘若 drawAnimation()的返回值more为 false,则表示动画已经结束。那么与动画开始相
对的,应该会调用 View.onAnimationEnd()。
看一下 View. draw( View Group, Canvas long)对这一
情况的处理
[View. java-->View.draw(View Group, Canvas, long)]
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ....... //到这里控件已经完成绘制过程
    if (a != null && !more){
        ......
        /*很意外,这里并没有直接调用View.onAnmationEnd(),
          而使用父控件的finishAnimatingView()*/
        parent.finishAnimatingView(this, a);
    }
    ......
    return more;
}
为什么动画结束的操作要交由父控件完成呢?先看一下 finishAnimatingView()的实现
[ViewGroup.java --> ViewGroup.finishAnimatingView()]
void finishAnimatingview(final viewview, Animationanimation){
    final ArrayList disappearingchildren mDisappearingchildren;
    /*①mDisappearingchildren的存在就是当动画结束时的操作必须交由父控件完成的原因。
      原来,如果在进行动画 将这个控件从父控件中移除时, ViewGroup会将其从mChildren中移除,但会同时
      将其置到 mDisappearingChildren数组中,并等待动画结束。
由于 mDisappearingchildren中的控
      件依然会得到绘制(参考 ViewGroup.dispatchDraw())。因此在执行了 ViewGroup.remaveView()
      之后,用户仍然可以看到动画中的控件,直到动画结束后控件才会消失。另外,前面提到的 LayoutTransition
      也依赖于这一机制,使得其移出动画西被用户看到
*/
    if (disappearingchildren != null) {
        if (disappearingchildren.contains(view)) {
            //把控件从 mDisappearingchildren中删除,这样一来控件真正地被移除了
            disappearingchildren,.remove(view);
            /*动画执行过程中将控件从父控件中移除并不会立刻触发 onDetachedFromWindow()
              而是当动画完成之后才会调用。这是因为控件的绘制依赖于 mAttachinfo
*/
            if (View.mAttachInfo != null) {
                view.dispatchDetachedFromwindow();
            }
            // clearAnimation()终止动画并将 mCurrentAnimation置为null 
            view.clearAnimation();

            mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
        }
    }
    /*clearAnimation()将会终止动画并将 mCurrentAnimation设置为null。于是下次重绘时没有
      mCurrentAnimation,便不会产生动画变换,因而控件恢复到了动画执行前的状态
      注意仅当 Animation.getFillAfter()为false时才会这么做。因为FillAfer表示当动画结束时将
      会使控作停在最后一帧的状态,因此必须保留 mCurrentAnimation使得动面变换持续生效
*/
    if (animation != null && !animation.getFillAfter()){
        View.clearAnimation();
    }
    //③最后调用View.onAndmationEnd(),动画终止
    if ((view.mFrivateFlags & PFLAG_ANIMATION_STARTED)== PFLAG_ANIMATION_STARTED){
        View.onAnimationEnd();
        ......
    }
}
控件动画的工作过程就是如此,开始于view. startAnimation(),计算于 View.drawAnimation(),
绘制于 View.draw(ViewGroup, Cavas,long),终结于 ViewGroup.finishAnimatingView()。
其核心工作是 Animation.getTransformation()以及控件的坐标系变换,这与WMS的窗口动画
的实现十分一致。

6.4.8绘制控件树的总结

      关于控件树绘制的介绍至此结束。这一节讨论了软件/硬件加速绘制、绘图缓存的原理
以及控件动画的原理4个方面的内容。控件树的绘制是一个先根遍历的过程,控件首先绘制
其自身的内容,然后再将绘制操作传递给它的每一个子控件,因此控件书的绘制工作可以分
为如下两个基本过程:

      口  控件自身的绘制:由draw(Canvas)完成,包括背量绘制、onDraw以及绘制边界效果
      口  将绘制传递给子控件:从 dispatchDraw()开始到draw( View Group. Canvas, long)为止,
            包括坐标系变换、缓存处理、动画计算等。

      另外,由于绘制是一个开销较大的操作,因此在相关的代码中对效率的优化随处可见
Android曾经饱受诟病的画面流畅度问题得以改善,不仅仅得益于硬件加速绘制、三重缓冲以
及 VSYNC的引人,软件代码中精益求精的优化一样功不可没。读者可以在对控件树绘制的
学习中仔细体会 Android在效率优化上的思想与苦心。

6.5深入理解输入事件的派发

      本节讨论控件系统中另一个重要的话题—输人事件派发。本书第5章讨论了输入事件
从设备节点开始一路经过 InputReader、 InputDispatcher再到 InputEventReceiver为正的加工与
派发的过程,不过当时所讨论的派发是以窗口为目标,并没有介绍输入事件如何从窗口传递
到指定的控件。本节将继续第5章的内容,以 InputEventReceiver为起点介绍输入事件在控件
树中的派发与处理。

      另外,在View类中有很多手段可以用于输入事件的处理,如 dispatchKeyEvent()、onTouch()
以及OnKeyListener等。 Activity、 Dialog等也有类似的方式可以处理输入事件。它们的优先级
特点以及区别等往往使开发者感到困扰。经过本节的学习读者可以对它们拥有清晰的认识
控件树中的输入事件派发是由 ViewRootlmpl为起点,沿着控件树一层一层传递给目标控
件,最终再回到 ViewRootImpl的一个环形过程。这一过程发生在创建 ViewRootlmpl的主线
程之上,但是却独立于 ViewRootlmpl.performTraversals()之外,就是说输入事件的派发并不
依赖于 ViewRootlmpl的“心跳”作为动力,而是有它自己的动力源泉。
经过第5章的学习可
以知道,这一动力源泉来自用于构建 InputEventReceiver的 Looper,当一个输入事件被派发给
ViewRootlmpl所在的窗口时, Looper会被唤醒并触发 InputEventReciever.onInputEvent()回调
控件树的输入事件派发便起始于这一回调。

      在正式讨论派发过程之前,首先需要讨论对派发过程有着决定性影响的两个概念—触
摸模式以及焦点。

6.5.1触摸模式

      在早期的键盘操作方式的移动设备中,用户选择某项操作的方式以方向键+确定键为主。
用户可以通过点击方向键在若干个操作项中进行选择(如菜单和经典的九宫格).并且被选中
的操作项会以背景高亮等方式突出显示,然后用户通过点击确定键执行被选中的操作。而现
今触摸方式已经成为操作移动设备的主要方式。在以触摸方式操作的界面中,并不存在被选
中的操作项的概念,而是通过直接点击期望的项目完成对应的操作。
      虽然在移动设备中使用按键操作似乎有些过时,不过在以办公为目的的设备中键盘的存
在仍会带来触摸方式不能提供的高效性与便利性。为此, Android同时支持按键与触摸两种操
作方式,并且可以非常自然地在二者之间进行切换。早期的 Android设备都提供了方向键与
确认键,而现有的 Android设备虽然都已不再提供这些按键,但是依然可以通过外接键盘的
方式(USB或者蓝牙)实现键盘操作。

      事实上, Android项目最初是为了开发一个用在键盘手机之上的智能手机系统,不
过在 iPhone发布之后, Android迅速改变了设计意图使之可以支持触屏操作。所以
Android可以同时支持键盘与触摸两种操作方式更是理所当然了。
      为了同时支持这两种模式,必须清楚这两种操作模式的区别与共同点。可以从焦点的角
度讨论这一问题。以一个拥有若干项的菜单为例,在键盘操作方式中,当用户通过方向键选
中一个菜单项时,这一菜单项便会获得焦点(高亮显示),当点击确认键时这一按键的事件被
派发给拥有焦点的菜单项,进而执行相应的动作。在这种模式下,必定有一个菜单项处于焦
点状态,以便用户知道按下确认键后会发生什么事情。而在触摸方式下,菜单项不会获取焦
点,而是直接响应触摸事件执行相应的动作。这种模式下不需要任何一个菜单项处于焦点状
态,因为用户会通过点击选择自己希望的操作,相反,倘若有一个菜单项处于高亮状态反而
会使用户产生迷惑而使得悬空的手指点不下去。二者也有共同点,例如一个文本框,无论在
哪种操作方式下,它都可以获得焦点以接受用户的输人。也就是说可以获取焦点的控件分为
两类:
      口  在任何情况下都可以获取焦点的控件,如文本框
      口  仅在键盘操作时可以获取焦点的控件,如菜单项、按钮等。
触摸模式( TouchMode)正是为管理二者的差异而引入的概念, Android通过进入或退
出触摸模式实现在二者之间的无缝切换。在非触摸模式下,文本框、按钮、菜单项等都可
以获取焦点,并且可以通过方向键使得焦点在这些控件之间游走。而在进入触摸模式后
某些控件如菜单项、按钮将不再可以保持或获取焦点,而文本框则仍然可以保持或获取
焦点。
      触摸模式是一个系统级的概念,就是说会对所有窗口产生影响。系统是否处于触摸模式
取决于WMS中的一个成员变量 mInTouchMode,而确定是否进入或者退出触摸模式则取决于
用户对某一个窗口所执行的操作
      导致退出触摸模式的操作有:
            口  用户按下了方向键。
            口  用户通过键盘按下了一个字母键(A、B、C、D等按键)。
            口  开发者执行了 View. requestFocusFromTouch()。
      而进人触摸模式的操作只有一个,就是用户在窗口上进行了点击操作

      窗口的 ViewRootlmpl会识别上述操作,然后通过WMS的接口 setInTouchMode()设置
WMS.mInTouchMode使得系统进入或退出触摸模式。
而当其他窗口进行 relayout操作时会在
WMS.relayoutWindow()的返回值中添加或删除 RELAYOUT_RES_IN_TOUCH_MODE标记使
得它们得知系统目前的操作模式。

      只有拥有 ViewRootlmpl的窗口才能影响触摸模式,或对触摸模式产生响应。通过
WMS的接口直接创建的窗口必须手动地维护触摸模式。

      当系统进入或退出触摸模式时会对控件系统产生怎样的影响呢?根据上述内答的介绍可
以得知它影响的是焦点的选择策略。通过接下来所讨论的控件树焦点相关的内容,可以使得
读者对触摸模式拥有更深入的理解。

6.5.2控件焦点

      和第5章所讨论的窗口焦点类似,控件的焦点影响了按键事件的派发。另外,控件的焦
点还影响了控件的表现形式,拥有焦点的控件往往会高亮显示以区别其他控件。

1.获取焦点的条件

      控件获取焦点的方式有很多种,例如从控件树中按照一定策略查找到某个控件并使其获
得焦点,或者用户通过方向键选择某个控件使其获得焦点等。而最基本的方式是通过
View.request Focus()。本节将通过介绍 View. requestFous()的实现原理揭示控件系统管理焦点的
方式
      View.requestFocus()的实现有两种,即View和 ViewGroup的实现是不同的。当实例是
个View时,表示期望此View能够获取焦点。而当实例是一个 ViewGroup时,则会根据一定
的焦点选择策略选择其一个子控件或 ViewGroup本身作为焦点。
本小节将首先讨论实例是
个View时的情况以揭示控件系统管理焦点的方式,随后再讨论 ViewGroup下requestFocus()
方法的实现。
参考 requestFocus()代码:
[View java-->View.requestFocus()]

public final boolean requestFocus (){
    /*调用 requestFoscus()的一个重载。View.FOCUS_DOWN表示焦点的寻找方向。当本控件是一个ViewGroup时
      将会从左上角开始沿着这个方向查找可以获取焦点的子控件。不过在本例只讨论控件是一个View
      时的情况,此时该参数并无任何效果
*/
    return requestFocus(View.FOCUS_DOWN);
}

public final boolean requestFocus(int direction){
    /*继续调用另外一个重载,新的重中增加了一个Rect作为参数。此Rect表示了上一个焦点控件的区域。
      它表示从哪个位置开始沿着 direction所指定的方向查找焦点控件。仅当本控件是 ViewGroup时此参数
      才有意义
*/
    return requestFocus(direction, null);
}

public boolean requestFocus(int direction, Rect previouslyFocusedRect){
    /*requestFocus()的这一重载便是View和ViewGroup分道扬镳的地方。 
      requestFacusNOSearch()方法的意义就是无须查找,直接使本控件获取焦点
*/
    return requestFocusNoSearch(direction, previouslyFocusedRect);
}    

private boolean requestFocusNoSearch (int direction, Rect previouslyFocusedRect){
    //首先检查一下此控件是否符合拥有焦点的条件
    //①首先,此控件必须是 Focusable的。可以通过View.setFocusable()方法设置控件是否 focusable
    if (mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
            (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false;
    }
    //②再者,如果系统目前处于触摸模式,则要求此控件必须可以在触摸模式下拥有焦点
    if(isInTouchMode()&& (FOCUSABLE_IN_TOUCH_MODE ! =(mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
        return false;
    }
    /*③最后,如果任一父控件的 DescendantFocusaBability取值为 FOCUS_BLOCK_DESCENDANTS时,阻止
      此控件获取焦点。 hasAncestorThatBlocksDescendantFocus()会沿着控件树一路回测到整个控件树的
      根控件并逐一检查 DescendantFocusability特性的取值
*/
    if (hasAncestorThatBlocksDescendantFocus()){
        return false;
    }
    //④最后调用 handleFocusGainInternal()使此控件获得焦点
    
handleFocusGainInternal(direction,previouslyFocusedRect);
    return true;
}
      并不是所有控件都可以获取焦点的。控件系统通过 View.setFocusable()设置一个控件能
否获取焦点。 View.setFocusable会将 FOCUSABLE或者 NOT_FOCUSABLE标记加入
View.mViewFlags成员中。
      当 FOCUSABLE标记存在时也不一定能够获取焦点,例如虽然6.5.1节中所介绍的
菜单项拥有 FOCUSABLE标记,但是在触摸模式下它仍无法获取焦点。控件系统通过
View.setFocusableInTouchMode()以区分这类控件。View.setFocusableInTouchMode()会将
FOCUSABLE_IN_TOUCH_MODE标记加入或移出 View.mViewFlags。于是,当系统处于触摸
模式时,仅当拥有 FOCUSABLE_IN_TOUCH_MODE标记的控件才能获取焦点。
      最后,控件能否获取焦点还取决于它的父控件的一个特性 DescendantFocusability,这
特性描述了子控件与父控件之间的焦点获取策略
DescendantFocusability可以有三种取
值,其中一种为 FOCUS_BLOCK_DESCENDANTS。当父控件的这一特性取值为 FOCUS_
BLOCK_DESCENDANTS时,父控件将会阻止其子控件或子控件的子控件获取焦点。
在介绍
View Group. requestFocus0时将会对这一特性进行详细介绍
因此控件能否获取焦点的策略如下:
      口  当 NOT_FOCUSABLE标记位于 View.mViewFlags时,无法获取焦点。
      口  当控件的父控件的 DescendantFocusability取值为 FOCUS_BLOCK_DESCENDANTS
            时,无法获取焦点。
      口  当 FOCUSABL标记位于 View. mViewFlags时分为两种情况
            a)位于非触摸模式时,控件可以获取焦点。
            b)位于触摸模式时, View. mViewFlags中存在 FOCUSABLE_IN_TOUCH_MODE标记时
               可以获取焦点,否则不能获取焦点
接下来分析 View.handleFocusGainInternal()。

2.获取焦点

[View.java--> View.handleFocusGainInternal()]
void handleFocusGainInternal (int direction, Rect previouslyFocusedRect){
    if ((mPrivateFlags & PFLAG_FOCUSED) == 0){
        //①把 PFLAG_FOCUSED标记加入mPrivateFlags中。这便表示此控件已经拥有焦点了
        mPrivateFlags |=PFLAG_FOCUSED;
        /*②将这一变化通知其父控件。这一操作的主要目的是保证控件树中只有一个控件拥有焦点,
          并且在ViewRootImpl中触发一次“遍历”以便对控件树进行重绘
*/
        if( mParent!=null){
            mParent.requstChildFocus(this,this);
        }
        /*③通知对此控件焦点变化感兴趣的监听者。在这个方法中,View.onFocusLose()、
          OnFocusChangelistener.onFocuschange()都会被调用。另外,控件焦点决定了输入法的输入对象,
          因此InputMethodManager的 focusIn()和 focusOut()也会在这里被调用,以更新输入法的状态
*/
        onFocusChanged(true, direction, previouslyFocusedRect);
        //④更新控件的 Drawable状态。这将使得控件在随后的绘制中得以高亮显示
        refreshDrawableState();
        .......
    }
}
View. handleFocusGainInternalo方法的4个工作都在情理之中,首先将 PFLAG_FOCUSED
标记加入mPrivateFlags成员中以宣称此控件是焦点的所有者。然后通过 mParent.requestChildFocus()
将这一变化通知父控件以将焦点从上一个焦点控件中夺走,并触发一次重绘。
接着通过 View.onFocusChanged()方法将焦点变化通知给感兴趣的监听者。最后更新控件的
DrawableState以使控件的绘制内容反映焦点的变化。
      接下来讨论 mParent.requestChildFocus()的实现。 PFLAG_FOCUSED是一个控件是否
拥有焦点的最直接体现,然而这并不是焦点管理的全部。这一标记仅仅体现了焦点在个体级
别上的特性,而 mParent.requestChildFocus()则体现了焦点在控件树的级别上的特性

3.控件树中的焦点体系

      mParent.requestChildFocus()是一个定义在 ViewParent接口中的方法,其实现者为
ViewGroup及 ViewRootlmpl
 ViewGroup实现的目的之一是用于将焦点从上一个焦点控件手中
夺走,即将 PFLAG_FOCUSED标记从控件的 mPrivateFlags中移除。
而另一个目的则是将这
操作继续向控件树的根部进行回溯,直到 ViewRootlmpl, ViewRootlmpl的 requestChildFocus()
会将焦点控件保存起来备用,并引发一次“遍历”。
参考 ViewGroup.requestChildFocus()方法
的实现:
[ViewGroup.java-->View.requestChildFocus()]
public void requestchildFocus(view child, view focused){
    /*①如果上一个焦点控件就是这个ViewGroup,则通过调用View.unFocus()
      将PFLAG_FOCUSED标记移除,以释放焦点
*/
    super.unFocus();
    if (mFocused != child){
        /*②如果上一个焦点控件在这个ViewGroup所表示的控伴树之中,即mFocused不为null,
          则调用mFocused.unFocus()以释放焦点
*/
        if (mFocused != null){
            mFocused.unFocus();
        }
        /*③设置mFocused成员为child。注意child参数并不是实际拥有焦点的控件。
          而是此ViewGroup的直接子控件,同时它是实际拥有焦点的控件的父控件
*/
        mFocused = child;
    }
    if (mParent != null){
        /*④将这一操作继续向控件树的根部回溯。
          注意child参数是此 ViewGroup,而不是实际拥有焦点的focused
*/
        mParent.requestChildFocus(this,focused);
    }
}
      此方法中 mFocused是一个View类型的变量,它是控件树焦点管理的核心所在。围绕着
mFocused, ViewGroup.requestChildFocus()方法包含了新的焦点体系的建立过程,以及旧有焦
点体系的销毁过程。

      新的焦点体系的建立过程是通过在 ViewGroup.requestChildFocus()方法的回溯过程中进行
mFocused=child这一赋值操作完成的。当回溯完成后, mFocused=child将会建立起一个单向链
表,使得从根控件开始通过 mFocused成员可以沿着这一单向链表找到实际拥有焦点的控件
即实际拥有焦点的控件位于这个单向链表的尾端,如图6-22所示。

      旧有的焦点体系的销毁过程则是通过在回溯过程中调用 mFocused.unFocus()完成的。
unFocus()方法有ViewGroup和View两种实现。首先看一下 ViewGroup.unFocus()的实现

[ViewGroup.java->ViewGroup.unFocus()]
void unFocus(){
    if (mFocused == null){
        /*如果 mFocused为空,则表示此 ViewGroup位于 mFocused单向链表的尾端,即此ViewGroup是焦
          点的实际拥有者,因此调用View.unFocus()使此 ViewGroup放弃焦点
*/
        super.unFocus();
    } else {
        //否则将 unFocus()传递给链表的下一个控件
        mFocused.unFocus();
        //最后将 mFocused设置为null
        mFocused = null;

    }
}
      可见ViewGroup.unFocus()将 unFocus()调用沿着 mFocused 所描述的链表沿着控件树向
下遍历,直到焦点的实际拥有者。焦点的实际拥有者会调用 View.unFocus(),它会将 PFLAG_
FOCUSED移除,当然也少不了更新 DrawableState以及 onFocusChanged()方法的调用

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第18张图片

      以图6-22的控件树的焦点状态为例来描述旧有焦点体系的销毁以及新焦点体系的
建立过程。
当View2-1-1通过 View.requestFocus()尝试获取焦点时,首先会将 PFLAG_
FOCUSED标记加入其 mPrivateFlags成员中以声明其拥有焦点。然后调用 ViewGroup2-1的
requestChildFocus(),此时 ViewGroup2-1会尝试通过 unFocus()销毁旧有的焦点体系,但是
由于其 mFocused为null,它无法进行销毁,于是它将其 mFocused设置为View2-1-1后将
requestChildFocus()传递给 ViewGroup 2。此时 ViewGroup 2的 mFocused指向了 ViewGroup2-2,
于是调用 ViewGroup2-2的unFocus()进行旧有焦点体系的销毁工作。 ViewGroup2-2的
unFocus()将此操作传递给Vew2-2-2的 unFocus()以移除View2-2-2的 PFLAG_FOCUSED标
记,并将其 mFocused置为null。回到 ViewGroup2的 requestChildFocus()方法后, ViewGroup2
将其 mFocused重新指向到 ViewGroup 2-1。在这些工作完成后,图6-22所描述的焦点体系则
变为图6-23所示。

      总而言之,控件树的焦点管理分为两个部分:其一是描述个体级别的焦点状态的 PFLAG_
FOCUSED标记,用于表示一个控件是否拥有焦点;其二是描述控件树级别的焦点状态的
ViewGroup.mFocused成员,用于提供一条链接控件树的根控件到实际拥有焦点的子控件的单
向链表。
这条链表提供了在控件树中快速查找焦点控件的简便办法。另外,由于焦点的排他
性,当一个控件通过 requestFocus()获取焦点以创建新的焦点体系时伴随着旧有焦点体系的销
毁过程

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第19张图片

 

      View下有两个查询控件焦点状态的方法isFocused()、hasFocus(),二者的区别在于:
isFocused()表示的是狭义的焦点状态,即控件是否拥有 PFLAG_FOCUSED标记;
而 hasFocus()表示的是广义的焦点状态,即拥有 PFLAG_FOCUSED标记
或 mFocused不为空,可以理解为 hasFocus()表示焦点是否在其内部(自身拥有焦
点,或者拥有焦点的控件在其所代表的控件树中)。在图6-23中,所有灰色的控件的
hasFocus()返回值都为true,而仅有View2-1-1的 isFocused()返回值为true

      至此,相信读者已经对焦点的体系有了深刻理解。接下来的内容将会讨论一种稍微复杂
的情况,即尝试在 ViewGroup上调用 requestFocus()会发生什么

4. ViewGroup 的 requestFocus()

      在本节开始时曾经讨论了获取焦点的最基本方式是 View.requestFocus()。如果调用此方法
的实例是一个控件(非 ViewGroup),其意义非常明确,即希望此控件能够获取焦点。而倘若
调用此方法的实例是一个View Group时又当如何呢?本节将讨论这一问题。
      ViewGroup重写了 View.requestFocus(int direction, Rect previouslyFocusedRect)以应对这种
情况。参考如下代码:
[VIewGroup.java->ViewGroup.requestFocus()]
public boolean request Focus (int direction, Rect previouslyFocusedRect){
    //①首先获取ViewGroup的 Descendantfocusabilty特性的取值
    int descendantFocusability = getDescendantFocusability();
    //根据不同的 Descendantfocusabilty特性, requestFocus()会产生不同的效果
    switch (descendantFacusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            /*FOCUS_BLOCK_DESCENDANTS: ViewGroup将会阻止所有子控件获取焦点,
              于是调用View.requestFocus()尝试自已获取焦点*/
            return super.requestFocus(direction, previouslyFocusedRect);

        case FDCUS_BEFORE_DESCENDANTS:
            /*FOCUS_BEFORE_DESCEANDANTS: ViewGroup将有优先于子控件获取焦点的权利。因此会首
              先调用View.recuestFocus()尝试自己获取焦点,若自己不满足获取焦点的条件则通过调用
              onRequestFocusInDescendants()方法将获取焦点的请求转发给子控件
*/
            final boolean took = super.requestFocus(direction, previcuslyFocusedRect);
            return took?took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        case FOCUS AETER DESCENDANTS: 
            /*FOCUS_AFLER_DESCENDANTS;子控件将有优先于此 ViewGroup获取焦点的权利。因此会首
              先调用onRequestFocusInDescendants()尝试将获取焦点的请求转发给子控件。侧若所有子控件
              都无法获取焦点,再调用vew. requestEocus()尝试自己获取焦点*/

            final boolean took = onRequestFocusInDescendants (direction, previouslyFocusedRect);
            return took? took : super.requestFocus(direction, previouslyEocusedRect);
        default:
            ...... //抛出异常
    }
}
      可见,在 ViewGroup上调用 requestFocus()方法会根据其 DescendantsFocusability特性的
不同而产生三种可能的结果。开发者可以通过 ViewGroup.setDescendantFocusability()方法修改
这一特性
      在 FOCUS_BLOCK_DESCENDANTS特性下, ViewGroup将会拒绝所有子控件获取焦点
此时调用 ViewGroup.requestFocu()会产生唯一的结果,即 ViewGroup会尝试自己获取焦点。
此时的流程与 View. requestFocus()没有什么区别。
      而在其他两种特性下调用 ViewGroup.requestFocus()则会产生View.requestFocus()与
ViewGroup.onRequestFocusInDescendants()两种可能的结果,不同的特性下二者的优先级不同。
      onRequestFocusInDescendants()负责遍历其所有子控件,并将 requestFocus()转发给它们
参考其实现:
[ViewGroup.java-->ViewGroup.onRequestFocusInDescendants()]
protected boolean onRequestFocusInDescendants (int direction,Rect previously FocusedRect){
    /*此方法的目的是接照 direction参数所描述的方向在子控件列表中依次尝试使其获取焦点
      这里 direction所描述的方向并不是控件在屏幕上的位置,而是它们在mChildren列表中的位置
      因此 direction仅有按照索引递增(FOCUS_ FORWARD)或递减两种方向可选
*/
    int index, increment, end, count = mChildrencount;
    if ((direction & FOCUS_FORWARD) != 0){
        index =0; increment = 1; end = count;
    } else {
        index =count-1; increment = -1; end = -1;
    }

    final View[] children = mChildren;
    for (int i=index; i != end: i += increment) {
        View child = children[i];
        //首先子控件必须是可见的
        if ((child.mViewFlags & VISIBILITY_MASK)==VISIBLE){
            //调用子控件的 requestrocus(),如果子控件获取了焦点,则停止继续查找
            if (child.requestFocus(direction, previouslyFocusedRect)){
                return true;
            }
        }
    }
    return false;
}
      ViewGroup.onRequestFocusInDescendants()其实是一种最简单的焦点查找的算法。它按照
direction所指定的方向,在 mChildren列表中依次调用子控件的 requestFocus()方法,直到有
个子控件获取了焦点。另外,需要注意子控件有可能也是一个 ViewGroup,此时将会重复
本节所讨论的工作,直到找到一个符合获取焦点条件的控件并使其获得焦点为止。
      至此,焦点管理中的 requestFocus()已经介绍完成。与其相对的还有一个 clearFocus()方
法用于清除控件的焦点。 requestFocus()与 clearFocus()作为互为反作用的一对双胞胎,它们
的执行方式与 requestFocus()是一致的。只不过它的执行过程是销毁现有的焦点体系而已(移
除 PFLAG_FOCUSED以及将 mFocused设置为null)。需要注意的是,在现有的焦点体系
被销毁后,它还会调用 ViewRootlmpl.mView.requestFocus()方法设置一个新的焦点。根据
ViewGroup.requestFocus()的工作原理,这一行为会在控件树中寻找一个合适的控件并将焦点
给它。而如果所选中的控件正好是执行 clearFocus()的控件,那么它会重新获得焦点。

      ViewGroup.requestFocus()方法还有另外一个重要的用处,就像 View.clearFocus().最后
会设置新的焦点一样,当控件树被添加到 ViewRootlmpl之后也会调用 ViewRootImp.mView.requestFocus()
设置初始的焦点。

      接下来讨论关于焦点的另外一个重要话题,即下一个焦点控件的查找。

5.下一个焦点控件的查找

      当一个控件获取焦点之后,用户往往会通过按下方向键移动焦点到另一个控件上。这时
控件系统需要在控件树中指定的方向上寻找距离当前控件最近的一个控件,并将焦点赋子它。
与 ViewGroup.onRequestFocusInDescendants()方法按照控件在 mChildren数组中的顺序查找不
同,这一查找依赖于控件在窗口中的位置。这一工作由 View.focusSearch()方法完成。
参考代码如下:
[View. java->View.focusSearch()]
public view focusSearch (int direction) {
    if (mParent != null){
        //查找工作会交给父控件完成
        return mParent.focusSearch(this, direction);
    }else{
        /*如果控件没有父控件就直接返回null,毕竟一个控件没有添加到控件树中查找下一个焦点是没有
            意义的*/

        return null;
    }
}
View. focusSearch()会调用父控件的 focusSearch(View focused, int direction)方法,由父控
件决定下一个焦点控件是谁。参考其在 View Group中的实现
[ViewGroup.java-->focusSearch()]
public View focusSearch (View focused, int direction) {
    if (isRootNamespace()){
        /*①如果isRootNamespace()返回true,则表示这是一个根控件。此时 ViewGroup拥有整个控件
          树,因此它是负责焦点查找的最合适的人选。它使用了 FocusFinder工具类进行焦点查找*/
        return FocusFinder.getInstance().findNextFocus(this, focused, direction);
    } else if (mParent != null){
        //②如果这不是根控件,则继续向控件树的根部回
        return mParent.focusSearch(focused, direction);
    }

    return null;
}
     如果此 ViewGroup不是根控件,则继续向控件树的根部回溯,一直回溯到根控件后,便
使用 FocusFinder的 findNextFocu()方法查找下一个焦点。这个方法的三个参数的意义如下:
      口  this,即 root。 findNext Focus()方法通过这个参数获取整个控件树中所有的候选控件。
      口  focused,表示当前拥有焦点的控件。 findNextFocus()方法会以这个控件所在的位置开
            始查找。
      口  direction,表示了查找的方向。

参考 findNextFocus()方法的代码
[FocusFinder.java-->FocusFinder.findNextFocus()]
public final View findNextFocus(ViewGroup root, View focused, int direction) {
    return findNextFocus(root, focused, null, direction);
}
private view findNextFacus(ViewGroup root, View focused,Rect focusedRect, int direction) {
    View next = null;
    //①首先将尝试依照开发者的设置,选择下一个拥有焦点的控件
    if( focused!=null){
        next = findNextUserSpecifiedFocus(root, focused, direction);
    }
    if (next != null){
        return next;
    }
    /*②内置算法。倘若开发者没有为当前的焦点控件设置下一个拥有焦点的控件,
      将会使用控件系统内置的算法进行下一个焦点的查找
*/
    ArrayListsview> focusables = mTempList;
    try{
        focusables.clear();
        /*③将控件树中所有可以获取焦点的控件存储到 focueables列表中。后续的将会在这个列表中进行查找*/
        root.addFocusables(focusable, direction);
        if (!focusables.isEmpty()){
            //④调用 findNextFocus()的另一个重载完成查找
            next = findNextFocus(root, focused, focusedRect, direction, focusables);
        }
    } finally{
        focusables.clear();
    }
    return next
}
      FocusFinder.findNextFocus()会首先尝试通过 findNextUserSpecifiedFocus()获取由开发者
设置的下一个焦点控件。有时候控件系统内置的焦点查找算法并不能满足开发者的需求
此开发者可以通过 View.setNextFocusXXXId()方法设置此控件的下一个可获取焦点的控件
Id。其中XXX可以是Left、 Right、Top、 Bottom和 Forward,分别用来设置不同方向下的下
一个焦点控件。
findNextUserSpecifiedFocus()会在 focused上调用 getNextFocusXXXId()方法
获取对应的控件并返回。

      倘若开发者在指定方向上没有设置下一个焦点控件,则 findNextUserSpecifiedfocus()方法
会返回null
, findNextFocus()会使用内置的搜索算法进行查找。这个内置算法会首先将控件树
中所有可以获取焦点的控件添加到一个名为 focusable的列表中,并以这个列表作为焦点控件
的候选集合。这样做的目的并不仅仅是提高效率,更重要的是这个列表打破了控件在控件树
中的层次关系。它在一定程度上体现了焦点查找的一个原则,即控件在窗口上的位置是唯
查找依据,与控件在控件树中的层次无关。

      最后调用的 findNextFocus()的另一个重载将在 focusables列表中选出下一个焦点控件
参考以下实现:
[FocusFinder. java-->FocusFinder.findNextFocus()]
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect
                int direction, Arraylist focusables){
    //①首先需要确定查找的起始位置
    if(focused != null){
        ........
        /*当 focused不为null时,起始位置即 focused所在的位置。View.getFocusedRect()所返回
          的并不是控件的mLeft、mTop、 mRight、 mBottom。因为scroll的存在它们并不能反映控件的
          真实位置。View.getFocusedRect()会将Scroll所产生的偏移考虑在内,但是 Tranformation
          (如setScaledX()等设置)并没有计算在内,因此它们并不会影响焦点查找的结果*
/
        focused.getFocusedRect(focusedRect);
        /*View.getFocusedRect()所返回的结果基于View本身的坐标系。为了使得控件之间的位置可以
          比较,必须将其转换到根控件所在的多坐标系中
*/
        root.offsetDescendantRectToMyCoords( focused, tocusedRect);
    } else {
        if (focusedRect == null)(
            /*当 focusedRect为null时,表示查找在指定方向上的第一个可以获取焦点的控件。
              此时会以根控件的某个角所在位置作为起始位置。例如对Left和Up两个方向来说,起始位置会被
              设置为根控件的右下角这个点,而对Right和 Bottom来说,起始位置将会是根控件的左上角
*/
            ......
        }
    }
    //接下来便会根据不同的方向选择不同的查找算法
    switch (direction) {
        case View.FOCUS_FORWARD:
        case View.FOCUS_BACKWARD:
            /*②对FOCUS_FORWARD/FOCUS_BACKWARD来说将会选择相对位置进行查找。这种查找与控件位置无关
              它会逃择 focusables列表中索引近邻 focused的控件作为查找结果
*/
            return findNextFocusInRelativeDirection(focusables,
                            root, focused, focusedRect,direction);
        case View.FOCUS_UP:
        case View.FOCUS_DOWN:
        case View.FOCUS_LEET:
        case View.FOCUS_RIGHT:
            //③对于UP、DOwN、LEFT、RIGHT 4个方向会根据控件的实际位置进行查找
            return findNextFocusInAbsoluteDirection( focusable, root, focused,focusedRect,direction);
        default:
            throw new IllegalArgumentException("Unknown direction: " + direction);
    }
}
      在这个方法中首先确定了查找的起点位置,然后根据 direction参数的取值选择两种不同
的査找策略。为 FORWARD和 BACKWARD两种査找方向所选择的查找策略比较简单,即首
先确定 focused所表示的控件在 focusable列表中的索引index,然后选择在 focusable列表中
索引为 index+1或 index-1的两个控件之一作为查找结果。
因此使用这两种方向进行查找的结
果与 ViewGroup.onRequestFocusInDescendants()类似,它反映了控件在 ViewGroup.mChildren
列表中的顺序。而对于其他4种方向的查找则复杂得多。参考 findNext FocusIn Absolute Direc
tion的代码:
[FocusFinder.java-->FocusFinder.findNextFocusInAbsoluteDirection()]
View findNextFocusInAbsoluteDirection (ArrayList focusables,
                                ViewGroup root, View focused,
                                Rect focusedRect,int direction){
    //①首先确定第一个最佳候选控件的位置。 focusedRect即查找的起始位置
    mBestcandidateRect.set(focusedRect);
    ......
    //closest表示在指定的方向上距离起始位置最接近的一个控件
    View closest = null;
    int numFacusables = focusable.size();
    //遍历 focusables列表进行查找
    for(int i=0;i         View focusable = focusables.get(i);
        /*既然是查找下一个焦点控件,那么已经拥有焦点的控件自然不能算作候选者。
          另外根控件也不能作为候选对象
*/
        if (focusable == focused || focusable == root) continue;
        /*②与获取起始位置一样,获取候选控件的位置。将其位置转换到根控件的坐标系中,
          以便能够与起始位置进行比较
*/
        focusable.getFocusedRect(mOtherRect);
        root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
        /*③通过isBettercandidate()方法比较现有的 mBestCandidateRect与候选控件的位置。倘若
          侯选控件的位置更佳,则设置候选控件为closest,设置候选控件的位置为 mBesetcandidaterect
          如此往复,当所有候选控件都经过比较之后,closest便是最后的查找结果
*/
        if (isBetterCandidate(direction, focusedRect
                            mOtherRect, mBestCandidateRect)){
            mBestCandidateRect.set(mOtherRect);
            closest = focusable;
        }
    }
    //返回closest作为下一个焦点控件
    retrun closest;
}
      这个方法的实现非常直观。在遍历 focusables列表的过程中使用isBetterCandidate()方法
不断地将 mBestcandidateRect与候选控件的位置进行比较,并在遍历过程中保存最佳的候选
控件到 closest变量中。在遍历完成后, closest即下一个焦点。整个过程与插入排序非常相似。
那么 isBetterCandidate()方法又是如何确定两个位置谁更合适呢?由于其算法实现十分繁
琐并且难以理解,这里直接给出其比较原则:
      口  首先,与起始位置比较,倘若一个控件A位于指定方向上,而控件B位于指定方向
            的另外一侧,则控件A是更佳候选。如图6-24的原则1所示,以LEFT为查找方向
            时,由于控件B位于 Focused控件的右侧,因此控件A为更佳的候选。

      口  其次,将起始位置沿着查找方向延伸到无限远,形成的形式被称为BEAM一条杠。
            倘若一个控件A与BEAM存在交集,而另一个控件B没有,则与BEAM存在交集的
            控件A为更佳候选。如图6-24的原则2所示。

      口  最后,当无法通过BEAM确定更佳候选时(如两个控件与BEAM同时存在交集,或
            同时不存在交集),则通过比较两控件与焦点控件相邻边的中点的距离进行确定,距
            离近者为更佳候选。注意在进行距离计算时 FocusFinder为指定方向增加了一个权
            重,以LEFT方向查找为例,其距离计算公式为(13*dx+dx+dy*dy),就是说这个距
            离对于X方向的距离更加敏感。以图6-24的原则3为例,相对于控件A,控件B到
            Focused实际距离是更小的。但由于在进行计算时X方向的距离有了3.6倍的加成,
            因此其计算距离远大于控件A,由此推断控件A是更佳候选。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第20张图片

围绕BEAM的比较中(原则2)还有更细节的原则。例如,当一个控件与另一个方向
的BEAM有交集时另一个控件是更佳候选,因为前者可以通过另一个方向查找到。读
者可以通过阅读 Focus Finder beam Beats方法学习其细节

至此,下一个焦点控件的查找便结束了,总结其查找过程如下
      口  倘若开发者通过View. setNext FocusXXXId显式地指定了某一方向上下一个焦点控件
            的ld,使用这一ld所表示的控件作为下一个焦点控件
      口  当开发者在指定的方向上没有指定下一个焦点控件时,则采用控件系统内置的焦点查
            找算法进行查找
      口  对于 FORWARD/ BACKWARD两个查找方向,根据当前焦点控件在 focusable列表中
            的位置 index,将位于 index-1或 index+1的控件作为下一个焦点控件。
      口  对于LEFT、UP、RIGHT、DOWN4个查找方向,将使用 FocusFinder.isBetterCandidate()
          方法从 focusables列表中根据控件位置选择一个最佳候选作为下一个焦点控件。
在选出下一个焦点控件之后,便可以通过调用它的 requestFocus()方法将其设置为焦点控
件了。

      相对于提供一个新的工具类 FocusFinder,将查找下一个焦点的算法实现在 ViewGroup
中看起来是一个更加直观的做法。但是这样一来查找算法在实现过程中难免会和焦
点的体系结构藕合起来。将其独立到 FocusFinder工具类中使得其实现更加纯粹,而
且独立于焦点的体系结构之后使得其适用范围更加广泛。例如,开发者可以通过
FocusFinder.findNextFocus()获取控件A的下一个焦点控件,而此时控件A不一定需要
拥有焦点。假如这一算法与控件焦点的体系结构严重藕合,这一用法将是不存在的

6.控件焦点的总结

      至此,关于控件焦点的讨论便完成了。本节以 View.requestFocus()为起点深人探讨了控件
系统对焦点的管理方式及其体系结构,并在最后介绍了查找下一个焦点的算法实现。相信读
者对于控件焦点已经有了非常深刻的理解。这将为后续讨论输入事件派发的相关内容提供重
要的基础。

6.5.3输入事件派发的综述

      在第5章关于输入系统的探讨中可以发现,按键事件与触摸事件采取了两种不同的派发
策略。按键事件是基于焦点的派发,而触摸事件是基于位置的派发。控件系统中事件的派发
一样采取了这两种策略。在深入讨论这两种策略在控件系统中的实现之前,首先讨论一下二
者的共通内容— ViewRootlmpl处理输入事件的总体流程。
      第5章中介绍了输入系统的派发终点是 InputEventReceiver作为控件系统最高级别的管
理者, ViewRootImpl便是 InputEventReceiver的一个用户,它从 InputEventReceiver中获取事
件,然后将它们按照一定的流程派发给所有可能感兴趣的对象,包括View、 PhoneWindow、
Activity以及 Dialog等。因此本节的探讨将从 InputReceiver.onInputEvent()开始。

1. ViewRootlmpl的输入事件队列

      在 ViewRootImpl.setView()中,新的窗口被创建之后, ViewRootlmpl使用WMS分配的
InputChannel以及当前线程的 Looper一起创建了 InputEventReceiver的子类 WindowInputEventReceiver
的一个实例,并将其保存在 ViewRootlmp.mInputEventReceiver成员之中。这标志着
从设备驱动到本窗口的输入事件通道的正式建立。至此每当有输入事件到来时, View Rootlmpl
都可以通过 WindowInputEventReceiver.onInputEvent()回调得到这个事件并进行处理。
参考其
[ViewRootlmpl.java-->WindowlnputEventReceiver.onInputEvent()]
public void onInputEvent(InputEvent event){
    //通过 enqueueInputEvent将输入事件入队,注意第三个参数为true
    enqueueInputEvent(event, this, 0, true);
}
再看 enqueueInputEvent()的实现:
[ViewRootImpl.java-->View.enqueueinputEvent()]
void enqueue InputEvent (InputEvent event,InputEventReceiver receiver, int flags, boolean processImmediately){
    /*①将 InputEvent对应的InputEventReceiver封装为一个 QueuedInputEvent
      QueuedInputEvent将是输入事件在 ViewRootImpl中的存在形式
*/
    QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
    /*②将新建的QueuedIaputEvent追加到 mFirstPendingInputEvent所表示的一个单向链表之中。
      ViewRootImpl将会沿着链表从头至尾地逐个处理输入事件
*/
    QueuedInputEvent last = mFirstPendingInputevent;
    if (last == null){
        mFirstPendingInputEvent = q;
    }else{
        while (last.mNext != null){
            last = last.mNext;
        }
        last.mNext =  q;
    }
    if (processImmediately) {
        //③倘若第三个参数为true,则直接在当前线程中开始对输入事件的处理工作
        doProcessInputEvent();
    }else{
        //④否则将处理事件的请求发送给主线程的 Handler,随后进行处理
        scheduleProcessInputEvents();
    }
}
      此方法揭示了 ViewRootlmpl管理输入事件的方式。同 InputDispatcher一样,在 ViewRootlmpl
中也存在着一个输入事件队列 mFirstPendinglnputEvent。输入事件在队列中以 QueuedInputEvent
的形式存在。 QueuedInputEvent保存了输入事件的实例、接收事件的 InputEventReceiver,以及
一个next成员用于指向下一个 QueuedlnputEvent。

      注意此方法的第三个参数 processImmediately。对于从 InputEventReceiver收到的正常事
件来说,此参数永远为true,即入队的输入事件会立刻得到执行。而当此参数为flse时,则
会将事件的处理发送到主线程的 Handler中随后处理。推迟事件处理的原因是什么呢?原来
ViewRootImpl会将某些类型的输入事件转换成为另外一种输入事件,并将新的输入事件入队。
由于此时仍处于旧有事件的处理过程中,倘若立即处理新事件会导致输入事件的递归处理,
即前一个事件尚未处理完毕时开始了新的事件处理流程。为了避免这一情况,需要在入队时
将 processlmmediately参数设置为 false,在一切都完成之后再来处理新的事件

      ViewRootlmpl转换输入事件的一个例子是轨迹球( TrackBall)事件的处理。操作轨迹
球时在驱动和输入系统层面会产生 MotionEvent。ViewRootlmpl根据 MotionEvent.getSource()
得知这是一个来自轨迹球的事件后会根据其事件的数据将其转换为方向键
(DPAD)的 KeyEvent,并将其通过 enqueueInputEvent()入队随后处理。这也是轨迹球
的实际效果与方向键(或五向导航键)一致的原因。

接下来看 doProcessInputEvent()的实现
[ViewRootlmpl.java-->ViewRootImpl.doProcessInputEvent()]
void doProcessInputEvents(){
    //遍历整个输入事件队列,逐个处理这些事件
    while (mCurrentIoputEvent == null && mFirstPendingInputEvent != null) {
        QueuedInputEvent q = mFirstPendingInputevent;
        mFirstPendingInputEvent = q.mNext;
        q.mNext = null;
        //①正在处理的输入事件会被保存为 mCurrentInputEvent
        mCurrentInputEvent = q;
        //②deliverInputEvent()方法将会完成单个事件的整个处理流程
        deliverInputEvent(q);
    }
    .......
}
      显而易见, doProcesslnputEvents()方法直到将输入事
件队列中的所有事件处理完毕之前不会退出,换言之在所有输入事件处理完成之前它不会放
下对于主线程的占用权。
这种看似粗犷的处理方式其实大有深意。 ViewRootlmpl最繁重的
工作 performTraversals()“遍历”就发生在主线程之上,而引发这一“遍历”操作的最常见
的原因就是在输入事件处理时修改控件的内容。 doProcessInputEvents()这种粗犷的处理方式
使得 performTraversals()无法在单个输入事件处理后立刻得到执行,因输入事件所导致的
requestlayout()或 invalidate()操作会在输入事件全部处理完毕之后由一次 performTranversals()
统一完成。
当队列中存在较多事件时这种方式所带来的效率提升是不言而喻的

2.分道扬镳的事件处理

接下来分析 deliverlnput Even()的工作原理。参考代码如下:
[ViewRootlmpl.java--> ViewRootlmpl.deliverInputEvent()]
private void deliverInputEvent(QueuedInputEvent q){
    try{
        if (g.mEvent instanceof KeyEvent) {
            //处理按键事件
            deliverkeyEvent(q);

        }else{
            final int source = q.mEvent.getSource();
            if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0){
                //处理触摸事件
                deliverPointerEvent(q);

            } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0){
                //处理轨迹球事件
                deliverTrackballEvent(q);

            } else {
                //处理其他 Motion事件,如悬浮( HOVER)、游戏手柄等
                deliverGenericMotionEvent(q);

            }
        }
    }finally{......}
}
      可以看到在 deliverInputEven()方法中不同类型的输入事件的处理终于分道扬镳了。根据
InputEvent的子类类型或 Source的不同,分别用4个方法处理4种类型的事件
      口  deliverKeyEvent(),用于派发按键类型的事件。它选择的是基于焦点的派发策略。
      口  deliverPointerEvent(),用于派发标准的触摸事件。它选择的是基于位置的派发策略。
      口  deliverTrackballEvent(),用于派发轨迹球事件。它的实现比较特殊,在使用基于焦点
            的派发策略将事件派发之后,倘若没有任何一个派发目标处理此事件,它将会把事件
            转化为一个表示方向键的按键事件并添加到 ViewRootImpl的输入事件队列中。
      口  deliverGenericMotionEvent(),用于派发其他的 Motion事件。这里一个大杂烩,悬浮
            事件、游戏手柄等会在这里被处理。

      由于篇幅的原因,本节将只介绍 deliverKeyEvent()以及 deliverPointerEvent()两个最常见
的同时也是最具代表性的事件处理流程。其他类型事件的处理方式直接采用或借鉴了这两种
事件处理流程中所体现的思想和流程,感兴趣的读者可以自行研究。

3.共同的终点— finishInputEvent()

      无论 deliverInputEven()中分成了多少条不同的事件处理通道,应输入系统事件发送循
环的要求,最终都要汇聚到一个方法中 ViewRootImpl.finishInputEvent()。这个方法用于
向 InputDispatcher发送输入事件处理完毕的反馈,同时也标志着一条输入事件的处理流程的
终结。

参考 ViewRootlmpl.finishInputEvent()的实现:
[ViewRootlmpl.java-->View.finishInputEvent()]
private void finishInputEvent(QueuedInputEvent q, boolean handled) {
    /*倘若被完成的输入事件不是 mCurrentInputEyent,则抛出异常
      ViewRootImpl不允许事件的嵌套处理
*/
    if(q != mCurrentInputEvent){
        throw new IllegalstateException("finished input event out of order");
    }
    // ①回收输入事件,并向InputDispatcher发送反馈
    if(q.mReceiver != null){
        /*如果 mReceiver不为null,表示这是一个来自 InputDipatcher的事件,
          需要向InputDispatcher发送反馈。事件实例的回收由InputEventReceiver托管完成
*/
        q.mReceiver.finishInputEvent(q.mEvent, handled);
    } else{
        /*如果 mReceiver为null,表示这是 ViewRootImpl自行创建的事件,
          此时只要将事件实例回收即可,不需要惊动InputDispatcher
*/
        q.mEvent.recycleIfNeededAfterDispatch();
    }
    /*②回收不再有效的 QueuedInputEvent实例。被回收的实例会组成一个以 mQueuedInputEventPool为
      头部的单向链表中。下次使用obtainQueuedInputEvent()时可以复用这个实例
*/
    recyclequeuedInputEvent(q);
    //设置 mcurrentInputEvent为null
    mCurrentInputEvent = null;
    //如果队列中有了新的输入事件,则重新启动输入事件的派发
    if (mFirstPendingInputEvent != null){
        scheduleprocessInputEvents ();
    }
}
至此,输入事件在 ViewRootlmpl中从 onInputEvent()开始到 finishInputEvent()终结的总
体流程便终结了。不难得出输入事件在 ViewRootImpl中派发的总体流程如图6-25所示。
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第21张图片

接下来将深入探讨 deliver Key Event0与 deliver Pointer Evento的工作原理

6.5.4按键事件的派发

      本节讨论按键事件在控件系统中的派发流程。按键事件是基于焦点进行派发的,即拥有
焦点的控件将是事件的目标接受者,因此深刻理解控件焦点的管理原理非常重要。控件焦点
的体系结构使得从根控件开始可以通过 mFocused成员一路查找到最终的焦点控件,因此按键
事件的派发流程就是沿着 mFocused成员所构成的单向链表进行遍历的过程。这看似是一个十
分简单的工作,但是因为有两个扰局者   输入法以及 DecorView    的存在使得这个过程并
没有想象中的简单。所幸在 Activity及 Dialog之外的用例下,控件树中并不存在 DecorView,
因此本节暂不考虑 DecorView对事件派发所带来的影响。
参考 View Rootlmpl. deliverKey Event0方法的实现
[ViewRootImpl.java-->ViewRootImpl.deliverKeyEvent()]
private void deliverKeyEvent ( queuedInputEvent g) {
    //从 QueuedInputEvent中获取 KeyEvent
    final KeyEvent event =(KeyEvent)q.mEvent;
    .......
    /*首先按键事件会尝试派发给输入法。将按键事件派发给输入法有三个条件,其中mView不为null,以及
      mAdded为true好理解,它们分别表示 ViewRootImpl中存在一棵控件树,井且其所在窗口已经被创建。
      而第三个条件是 QueuedInputEvent.mFlags中不存在FLAG_ DELIVER_POST_IME标记。
      这一标记表示此按键事件之前已经派发给了输入法,但是输入法并没有消费它。所以当存在这一标记时无须
      再将事件派发给输入法
*/
    if(mView != null && mAdded &&
                (q.mFlags & QueuedInputEvent.FLAG_DELIVER_POST_IME)== 0) {
        /*①首先,View.dispatchKeyEventPreIme()方法将输入事件派发给控件树。
          这是控件树中的控件第一次有机会处理按键事件
*/
        if (mView.dispatchkeyEventPreIme (event)){
            //如果控件消费了这一事件,则结束派发工作
            finishInputEvent(q, true);
            return;
        }
        //②将按键事件派发给输入法。mLastWasImTarget表示此窗口可能是输入法的输入目标
        if (mLastWasImTarget) {
            InputMethodManager imm= InputMethodManager.peekInstance();
            if (imm != null) {
                /*通过InputMethodManager. dispatchkeyEvent()将事件派发给输入法。同时终止此次
                  事件的派发过程。注意此方法的最后一个参数 mInputMethodcallback,它是一个实现了
                  InputMethodManager.Finishedeventcallback口的回调。无论输入法是否消费这个
                  事件,此回调都会收到通知, ViewRootImpl可以在收到这一通知之后销毁此事件并结東
                  派发,或将事件重新派发给控件树
*/
                final int seq = event.getsequenceNumber();
                imm.dispatchKeyEvent(mView.getContext(),seq,
                                        event, mInputMethodCallback);
                //终止派发工作,对于此事件的后续处理将在 mInputMethodcallback回调中进行
                return;
            }
        }
    }
    /*③将输入事件派发给控件树。执行到这一步往往是由于QueuedInputEvent.mFlags中存在
      FLAG_DELIVER_POST_IME标记,而 deliverKeyEventPastIme()将会再次把事件派发给控件树。这是控件
      第二次有机会处理按健事件
*/
    deliverKeyEventPostIme(q);
    
}
      围绕着输入法, ViewRootimp.deliverKeyEvent()方法揭示了按键事件派发的三个阶段。首
先控件树中的控件可以在输入法处理按键事件之前,通过View.dispatchKeyEventPrelme()方法
获得处理机会。倘若控件并未在此时消费事件,那么按键事件将会被派发给输入法。倘若输
入法也没有消费这一事件,则 ViewRootlmpl.deliverKeyEventPostlme()将使得控件第二次有机
会处理此事件。
接下来将详细讨论这三个阶段的实现

1.按键事件的初次派发

      当按键事件第一次进入 ViewRootlmpl.deliverKeyEvent()方法时,很明显 FLAG_DELIVER_POST_IME
标记是不存在的。因此View.dispatchKeyEventPrelme()方法会被执行。从而
使得控件树中的控件可以优先于输入法获得事件的处理权利。
毕竟这个事件是派发给本窗口,
而不是输入法所在的窗口。
      同 View.requestFocus()方法一样,View.dispatchKeyEventPrelme()拥有 ViewGroup和View
两种不同的实现
,参考这两种实现的代码
[View.java-->View.dispatchKeyEvent Prelme()]
public boolean dispatchKeyEventPreIme (KeyEvent event){
    /*调用 onKeyPrelme()尝试消费这个事件。View.onPrelme()在View中是一个空的实现,直接返回
      false表示此控件并不希望在输入法之前消费此事件。View的子类可以重写这个方法以消费它,并
      通过返回true阻止输入法获得这一事件
*/
    return onKeyPreIme(event.getKeyCode(), event);
}
[ViewGroup.java-->ViewGroup.dispatchKeyEventPrelme()]
public boolean dispatchKeyEventPreIme(KeyEvent event) {
    if ((mPrivateFlags & (PFLAG_FOCUSED | PELAG_HAS_BOUNDS))
                    ==(PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)){
        /*如果此ViewGroup是焦点的拥有者,则直接调用View.dispatchKeyEventPrelme()
          尝试消费此事件
*/
        return super.dispatchKeyEventPreIme (event);
    } else if (mFocused !=null &&(mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        //倘若mFocused不为null,则把 dispatchKeyEventPreIme()传递给mFocused
        return mFocused.dispatchkeyEventPreIme (event);

    }
    return false;
}
      这两个实现都很简单,同时也体现了按键事件所采用的基于焦点的派发策略—按键事
件将由拥有焦点的控件进行处理。 ViewGroup的实现用于将事件沿着 mFocused链表向着焦点
控件所在的方向进行传递。而View的实现则通过调用 View.onKeyprelme()方法尝试进行事件
的消费。
      开发者可以通过重写 View.onKeyprelme()获得优先于输入法进行按键事件的处理。同
样, Android并不阻止开发者通过重写 View.dispatchKeyEventPrelme()做同样的事情。二者的
区别在于, dispatchKeyEventPrelme()将先于 onKeyPrelme()获得事件的处理权。另外更重要
的是, onKeyPreme()仅当控件拥有焦点时才会被调用
,而在 mFocused链表上的所有控件的
dispatchKeyEventPrelme()都会被调用。因此重写 dispatchKeyEventPrelme()往往用于在一个
ViewGroup中拦截特定的按键事件进行处理并阻止其子控件获得它。

      dispatchKeyEventPrelme() 与 onKeyPrelme()两个方法的区别同样适用于其他与输入事
件相关的 dispatchXXX()与 onXXX()。一般来讲, dispatchXXX()的作用是为了从根控
件开始将事件传递给目标控件,而 onXXX()则用于在目标控件中处理事件。

另外, ViewGroup得以将事件沿着 mFocused链表传递按键事件的另外一个条件是
PFLAG_HAS_BOUNDS标记的存在。 PFALG_HAS_BOUNDS表示控件的 mLeft/mTop/
mRight/mBotton已被 View.setFrame()方法进行了设置,即控件已经完成了 layout操
作。未经过 layout操作的控件可以理解为尚未初始化完毕,控件系统会拒绝这样的控
件获取事件。

2.输入法对按键事件的处理

      回到 ViewRootlmpl.deliverKeyEvent(),倘若没有任何控件在 View.dispatchKeyEventPreIme()
的过程中消费这一事件,那么它将被派发给输入法
      为什么按键事件需要派发给输入法呢?通过第5章关于输入事件的分析可知,按键
事件将会派发给处于焦点状态的窗口。而输入法所在的窗口是无法获取焦点的,因为它
的 LayoutParams.flags被InputMethodService放置了 FLAG_NOT_FOCUSABLE标记(参考
SoftlnputWindow.initDockWindow()方法的实现),因此在默认的派发机制下,输入法窗口是无
法获取按键事件的,包括其他非基于位置的事件如轨迹球事件等。不允许其获得焦点是由于
输入法仅仅是一个辅助工具,它的存在不应对目标窗口的功能或行为产生影响。然而输入法
确实拥有处理按键事件的需求,例如通过BACK键将输人法关闭,或通过方向键在输人法中
进行选词等。为了解决这一矛盾, ViewRootlmpl将会在收到事件后首先转发给输入法,当输
入法对此事件不感兴趣时再将其发送给控件树。不过,正如上一节所述,作为事件的正统接
收者,控件树可以通过重写 View. dispatchKeyEventPrelme()或 View. onKeyPrelme()先于输人法
处理事件。

      派发给输入法的条件是 mLastWasImTarget成员为true,即本窗口可能是输入法的输入目
标。这一成员的取值来自于窗口的 LayoutParams.flags中 FLAG_NOT_FOCUSABLE及
FLAG_ALT_FOCUSABLE_IM两个标记的存在情况。
读者可以参考 Window Manager. LayoutParam.
mayUselnputMethod()的实现了解这两个标记的功能。当本窗口不能作为输入法的输入目标
时,便不会有输入法覆盖其上,自然不需要输入法对按键事件进行处理。
      InputMethodManager.dispatchKeyEvent()方法将会通过Binder将按键事件发送给当前输入
法所在的 InputMethodService,并在那里的 onKeyXXX()系列事件处理方法中得到处理。这
过程的细节超出了本节的讨论范围,读者可以自行研究。
      在输入法完成事件的处理之后,无论是否消费了这一事件,都会通过 InputMethodCallback.
finishedEvent()回调将其处理结果通知 ViewRootlmpl。ViewRootlmpl会通过 handleImeFinishedEvent()
法继续对事件的派发工作。
参考实现如下:
[ViewRootlmpl java-->ViewRootlmpl.handlelmeFinishedEvent()]
void handleImeFinishedEvent (int seq, boolean handled) {
    final QueuedInputEvent q = mCurrentInputEvent;
    //继续派发工作的前提是seq必须与 mCurrentInputEvent一致,否则这一回调将会被忽略
    if(q != null && q.mEvent.getsequenceNumber()== seq) {
        if (handled){
            //①如果输入法消费了这一事件,则终止此事件的派发工作
            finishInputEvent (q, true);
        }else{
            if (q.mEvent instanceof KeyEvent) {    
                KeyEvent event = (KeyEvent)q.mEvent;
                ......
                //②通过deliverKeyEventPostIme()将输入事件重新派发给控件树
                deliverkeyEventPostIme(q);
            } else{
                .....//其他类型的输入事件
            }
        }
    }else{.....}
}
      显而易见,当输入法完成事件处理后, ViewRootlmpl有两种可能的处理:倘若事件已被
输入法消费,则直接终止后续的派发工作;反之,则通过 deliverKeyEventPostlme()将事件重
新派发给控件树。

3.按键事件的最终派发

      deliverKeyEventPostlme()负责按键事件的最终派发。在这里,开发者最熟悉的
View.onKeyDown()系列回调以及 OnKeyListener监听者都会得到触发。
同时一些系统内置的按键功
能也将在这里进行处理。参考实现如下
[View RootImpl java--> ViewRootImpl.deliverKeyEventPostIme()]
private void deliverKeyEventPostIme (QueuedInputEvent q ){
    final KeyEvent event =(KeyEvent)q.mEvent;
    ......
    /*①首先,检查此按键事件是否会退出触摸模式。一般来说,方向键、字母键的按下事件都标识着用户将会
      以按键的方式操纵 Android,此时需要退出触模模式
*/
    if(checkEorLeavingTouchModeAndconsume(event)) {
        //当这一事件导致触摸模式的退出时,意味着此事件已经被消费了,因此终止这一事件的派发工作
        finishInputEvent (q, true);
        return;
    }
    //②在正式开始事件的派发之前,首先让mFallbackBvontHandler过目一下
    mFallbackEventHandler.preDispatchKeyEvent (event);

    //③将事件派发给控件树。这是本方法最重要的工作
    if (mView.dispatchKeyEvent(event)){
        //如果有控件消费了这一事件,则终止事件的派发
        finishInputEvent(q, true);

        return;
    }
    .......
    //④如果没有控件消费这一事件,则尝试将事件派发给mFallbackEventHandler
    if (mFallbackEventHandler.dispatchKeyEvent(event)){
        //如果事件被消费,则终止事件的派发
        finishInputEvent(q, true);
        return;
    }
    //⑤处理方向键的按下事件。用于使焦点在控件之间游走
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        int direction = 0;
        //根据KeyEvent.getKeyCode()确定焦点游走的方向
        switch (event. getKeyCode()){
            case KeyEvent.KEYCODE_DPAD_LEFT:
                if (event.hasNoModifiers()) {
                    /*按下左键,表示将焦点移动到当前焦点控件左侧的控件上。
                      KeyEvent.hasNoModifiers()表示此时Alt/Ctrl/Shift没有按下
*/
                    direction = View.FOCUS_LEFT;
                }
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                ......//识别其他方向键
        }
        if (direction ! = 0){
            //获取当前的焦点控件
            View focused = mView.findFocus();
            if(focused != null){
                /*熟悉的View. focusSearch()方法,它将根据6.5.2节中所介绍的算法查找下一个应当
                  获取焦点的控件
*/
                View v = focused.focusSearch(direction);
                if (v != null && v != focused){
                    .......
                    //通过View.requestFocus()方法使此控件获取焦点
                    if(v.requestFocus(direction, mTempRect)){
                        ......
                        //结束事件的派发
                        finishInputEvent(q,true);

                        return;
                    }
                }
                .......
            }
        }
    }
    //最终,此事件没有任何一个对象对其感兴趣,终止事件的派发
    finishInputEvent(q, false);

}
      可以说, View.dispatchKeyEventPrelme()以及 InputMethodManager.dispatchKeyEvent()
都是按键事件派发的前奏而已。 ViewRootlmpl.deliverKeyEventPostlme()才是重头戏。在这个
方法中,可以根据优先级列出如下几个可能消费事件的对象或行为:
      口  TouchMode。如果导致了触摸模式的终止,此事件会被消费。
      口  控件树中的控件。 View.dispatchKeyEvent()方法会将事件派发给控件树。
      口  mFallbackEventHandler。它在 ViewRootlmpl的构造函数中通过 PolicyManager.
            makeFallbackEventHandler()创建,是一个 PhoneFallbackEventHandler类的实例。
            与 PhoneWindowManager类似,它提供了一个进行系统级按键处理的场所,只不
            过它的处理优先级低得多
,当需要为某个按键定义一个系统级的功能,并允许应用程
            序修改此按键的功能时,可以在 PhoneFallbackEventhandler类中进行实现,例如使用
            音量键调整系统音量的工作就在这里完成。因此应用程序可以将音量键挪作他用,例
            如在相机中用来调整焦距。需要注意的是,与 PhoneWindowManager在系统中只有一
            个实例不同,每个 ViewRootImpl都有各自的 PhoneFallbackEventhandler实例,因此它
            并不适合存储一些系统级的状态。

      口  焦点游走。它主要感兴趣的是方向键和TAB键的按下事件,它将根据按键选择一个
            焦点的查找方向,然后通过 View.focusSearch()方法选择一个控件并使其获得焦点。
其中,最感兴趣的内容自然是负责将事件派发给控件树的View.dispatchKeyEvent()。它与
View.dispatchKeyEventPrelme()十分相似,拥有 ViewGroup与View两种不同的实现
,参考它
们的代码
[View.java-->View.dispatchKeyEvent()]
public boolean dispatchKeyEvent (KeyEvent event){
    .......
    //①首先由 OnKeyListener监听者尝试处理事件。它可以通View.setOnkeylistener()进行设置
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnkeyListener != null && (mViewFlags && ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        //如果 OnkeyListener消费了事件则返回true
        return true;
    }
    //②通过event.dispatch()方法将事件发送给View的指定回调,如 onkeyDown()/ onkeyup()等
    if (event.dispatch(this, mAttachInto != null ?
        mAttachInfo.mKeyDispatchstate : null,this)){
        //如果控件的 onKeyDown()/ onKeyUp()等回调消费了事件则返回true
        return true;
    }
    //此控件没有消费这个事件
    return false;
}
[ViewGroup.java-->ViewGroup.dispatchKeyEvent()]
public boolean dispatchKeyEvent (KeyEvent event) {
    .......
    if ((mPrivateFlags &(PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)
            == (PFLAG_FOCUSED | PFLAG_HAS_BCUNDS)){
        //①如果此 ViewGroup拥有焦点,则调用 View.dispatchkeyEvent()尝试消费事件
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused !=nul1 && (mFocused.mPrivateFlags & PELAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS){
        //②倘若此ViewGroup不拥有焦点,则将事件沿着 mFocused链表进行传递
        if (mFocused.dispatchKeyEvent(event)){
            return true;
        }
    }
    ......
    return false;
}
      显而易见, ViewGroup.dispatchKeyEvent()的实现与 ViewGroup.dispatchKeyEventPrelme()
别无二致。它将按键事件沿着 mFocused链表向尾端传递并沿途调用 ViewGroup.dispatchKeyEvent()
方法。并最终由焦点拥有者的 View. dispatchKey Event0尝试事件的消费。
      View.dispatchKeyEvent()的本质与 View.dispatchKeyEventPreIme()一致,只不过由于事件
处理回调的不同而有所差别。在这个方法里,通过View.setOnKeyListenert()所设置的监听者
具有较高的优先级,当这个监听者对事件不感兴趣时,通过 KeyEvent.dispatch()方法引发的
View.onKeyDown()/onKeyUp()才有机会进行事件的处理

4.按键事件派发的总结

至此,关于按键事件的派发过程的介绍便结束了。总结其派发与处理流程如下:
    口  首先按键事件将通过 View.dispatchKeyEventPrelme()派发给控件树。此时开发者可以
        进行事件处理的场所是 View.dispatchKeyEventPreIme()或View.onKeyPreIme()
    口  然后按键事件将通过 InputManager.dispatchKeyEvent()派发给输入法。此时处理事件
        的场所是当前输入法所在的 InputMethodService的 onKeyDown()/onKeyUp()等系列回调
    口  之后按键事件将被 View.checkForLeavingTouchModeAndConsume()方法用来尝试退出
        触摸模式。
    口  再之后按键事件将被 View.dispatchKeyEvent()在此派发给控件树。此时开发者可以进
        行事件处理的场所是 View.dispatchKeyEvent(),通过vew. setOnKeyListener()方法设
        置的OnKeylistener,以及 View.onKeyDown()/ onKeyUp()等系列回调。
    口  PhoneFallbackEventhandler将在上述对象都没有消费事件时尝试对事件进行处理
    口  最后 ViewRootlmpl将尝试通过按键事件使焦点在控件之间游走。
      另外,按键事件是基于焦点进行派发的。事件将从根控件开始沿着 mFocused链表向拥
有焦点的控件进行传递,沿途的 ViewGroup都将有机会通过重写其 dispatchKeyEventPrelme()
或 dispatchKeyEvent()方法进行拦截处理。而 onKeyPrelme()/onKeyDown()等系列回调仅会发
生在拥有焦点的控件上。
      另外,如果窗口属于一个 Activity或者 Dialog,其根控件是一个 Decorview,它的
dispatchKeyEvent方法还会尝试将事件派发给其他组件。这部分内容将在6.6.1节介绍。

6.5.5触摸事件的派发

      触摸事件是基于位置进行派发的。相对于按键事件可以通过 mFocused链表进行传递并最
终到达焦点控件,触摸事件的派发过程由于坐标系变换、多点触摸的存在而复杂得多。另外
ViewRootlmpl并不需要将触摸事件派发给输入法,因为 InputDispatcher会将点击到输入法的
窗口的事件直接派发给它,因而不需要 ViewRootlmpl转发。

触摸事件派发的起点是ViewRootimpl. dispatchPointerEvent()。参考实现如下:
[View Rootlmpl java-->ViewRootlmpl.dispatchPointerEvent()]
private void deliverPointerEvent(QueuedInputEvent q){
    final MotionEvent event = (MotionEvent)q.mEvent;
    final boolean isTouchEvent = event.isTouchEvent();
    ......
    //①当这是一个按下事件时,将会进入触摸模式。此时将会重新设置焦点控件
    final int action = event.getAction();
    if (action== MotionEvent.ACTION_DOWN || action = MotionEvent.ACTION_SCROLL){
        /*ensureTouchMode()负责进入或退出触摸模式,它会重新设置焦点控件,并将触摸模式同步到WMS
          以便以后所创建的窗口可以从WMS得知应当工作在何种模式下
*/
        ensureTouchMode(true);
    }
    /*ViewRootimpl所收到的触摸事件位于窗口的坐标系下。将其派发给根控件时需要将其坐标转换到根
      控件下。根控件的坐标系与窗口坐标系的区别在于Y方向上的滚动量mCurScrollY
*/
    if (mCurScrollY !=0){
        event.offsetLocation(0, mCurScrollY);
    }
    ......
    //②将触摸事件派发给控件树
    boolean handled= mView.diapatchPointerEvent(event);

    if (handled) {
        //事件已被消费,结束派发工作
        finishInputEvent(q, true);
        return;
    }
    //事件没有被消费,结束澈发工作
    finishInputEvent(q, false);

}
由于不需要将事件派发给输入法, ViewRootlmpl.deliverPointerEvent()的工作相比
deliverKeyEvent()要简单得多。一是强制进入触摸模式,二是通过 View.dispatchPointerEvent()
将触摸事件派发给控件树。
再看view. dispatchpointer EventO的实现
[View. java--> View.dispatchPointerEvent()]
public final boolean dispatchPointerEvent(MotionEvent event){
    if (event.isTouchEvent()){
        //如果是一个触摸事件,则通过dispatchTouchEvent(event)进行派发
        return diapatchTouchEvent(event);

    } else {
        //否则通过 dispatchGenericMotionEvent(event)进行派发
        return dispatchGenericMotionEvent(event);

    }
}
      这里所谓的 PointerEvent其实包含以 MotionEvent.getAction()进行区分的两种事件:一种
是实际的触摸事件如 ACTION_DOWN/MOVE/UP等表示实际接触到屏幕所产生的事件,而
另外一种则是 ACTION_HOVER_ENTER/MOVE/UP未接触到屏幕所产生的事件
其中第
种事件才是真正意义上的触摸事件,本节将以此类事件为例进行探讨,即主要分析Vew
dispatch Touch Event()的实现。至于 HOVER类型的实现,其派发思想与实现原理与触摸事件
的派发十分相似,读者可以类比研究。

1. MotionEvent与触摸事件的序列

      触摸事件的派发十分复杂,因此有必要在继续分析代码之前先介绍一下关于触摸事件派
发的基本知识及其派发思想,这样才不会被代码中复杂的逻辑所迷惑。
      触摸事件被封装为一个继承自 InputEvent类的 MotionEvent中。它包含了多种用于描述
次触摸的详细信息,其中最基本的两个信息是通过 gelAtion()方法获取的动作信息以及
通过 getX()/getY()方法所获得的位置信息。
由于多点触摸的存在,这几个方法在实际的使用
过程中与 KeyEvent类中的 getAction()以及 getKeyCode()有所不同。首先通过 MotionEvent.
getAction()所获得的动作信息是一个复合值,它在低8位描述了实际的动作如 ACTION_
DOWN、 ACTION_UP等,而在其9~16位描述了引发此事件的触控点从0开始的索引号

因此在实际的使用过程中,往往需要将这两个信息分离出来。开发者可以通过 MotionEvent.
getActionMasked()获取实际的动作,然后通过 MotionEvent.getActionIndex()获取此事件所代
表的触控点的索引号。
另外,虽然一个 MotionEvent由一个触控点所引发,然而它却包含了所
有触控点的位置信息,以便开发者可以在收到一个MotionEvent时根据所有触控点的信息进行
计算与决策,因此 MotionEvent. getX()与getY()两个方法可以接受触控点的索引号作为参数,
以返回特定触摸点的触摸位置。
      触控点的索引号是什么呢?在MotionEvent的内部有一个数组 mSamplePointerCoords,其
每一个元素是一个 PointerProperties结构体,描述了一个触控点的信息。所谓的索引号就是一
个触控点在这个数组中所在的位置。在多点触摸的过程中,伴随着用户手指的抬起与按下,
一个触控点在数组中的位置不是一成不变的。例如用户所按下的第二个点B的索引号为1,
当用户首先抬起其所按下的第一个点A(其索引号为0)之后,A在mSamplePointerCoords中
的信息会被删除,从而使得点B的索引号变为0,因此触控点的索引号并不能用来识别或追
踪一个特定触控点。开发者需要通过触控点的ID达到识别或追踪一个特定触控点的目的。触
控点的ID存储在 PointerProperties结构体中,可以通过 MotionEvent.getPointerld()方法获得。
作为触控点的属性之一,与 getX()/getY()类似,这一方法需要索引号作为参数。

      可见 MotionEvent的使用远比 KeyEvent复杂。一般而言,开发者在收到一个 MotionEvent
之后,首先需要通过 MotionEvent.getActionMasked()获取其实际的动作,然后通过
MotionEvent.getPointerIndex()获取引发这一事件的触控点的索引号,然后再根据索引号获取触控点
的坐标信息,以及其ID。
开发者最终关心的信息是实际的动作,坐标信息以及触控点的ID
索引号只不过是获取这些信息的一个临时的工具而已

      当 MotionEvent所携带的动作是 ACTION_MOVE时,共getAction()所获得的动
作信息并不包含触控点的索引,因为 ACTION_MOVE并不会导致增加或减少触控
点,不过它仍然保存了所有触控点的位置、ID等信息。开发者可以通过 MotionEvent.
getPointerCount()得知此时有多少触控点处于活动状态,并通过一个for循环遍历每一
个触控点的位置、ID信息。从这一事实可以得知, getAction()中所包含的触控点索引号其
实是为了通知开发者是否产生了新的触控点(按下),或某个触控点被移除(抬起)。

      接下来是触摸事件的序列它是用户从第一个手指按下开始到最后一个手指拾起这一过
程中所产生的 MotionEvent序列。最简的情况是单点触摸的事件序列,它从一个 ACTION_DOWN
开始,经历一系列的 ACTION_MOVE,以一个 ACTION_UP结束。而多点触摸时这
序列则复杂一些,它同样以一个 ACTION_DOWN开始,经历一系列的 ACTION_MOVE,
而当用户另一个手指按下时会产生一个 ACTION_POINTER_DOWN,当某一手指抬起时会
产生一个 ACTION_POINTER_UP,当最后一个手指抬起时,以一个 ACTION_UP结束事件
序列。
在这个过程中,开发者可以通过事件所携带的触控点的ID追踪某一个触控点的始末。
图6-26描述了拥有三个触控点的事件序列,从中可以看出,序列以 ACTION_DOWN开始以
ACTION_UP结束,某一触控点在中途的按下与抬起由 ACTION_POINTER_DOWN/UP事件
表示,它们在 getAction()中所携带的索引号指示了发生这一动作的触控点的索引,并且这
索引随着当前处于活动状态的触控点的数量的变化而变化,但是触控点的ID则始终保持不
变。同时整个事件序列中每一个事件都包含了所有触控点的信息。
《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第22张图片

      不难想到,多点触摸下的事件序列中每一个触控点的信息都足以独立形成一个事件序列。
以图6-26为例,整个事件序列可以通过某种手段将触控点1拆分出来,从而形成两条新的
事件序列,即触控点1的信息所组成的一条单点序列,以及由触控点2和3所组成的一条双
点序列。这两条序列被称为原始序列的子序列,而这一行为则被称为事件序列的拆分( Split)。
事件序列的拆分是通过拆分序列中的每一个 MotionEvent实现的。
MotionEvent的拆分可以通
MotionEvent.split()方法完成,它可以从当前 MotionEvent中产生一个新的仅包含特定触控
点信息的 MotionEvent
,而这个新产生的 MotionEvent则成为子序列的一部分。为什么会出现
事件序列的拆分呢?参考图6-27的左图,一个 ViewGroup中包含两个控件,当用户的两个手
指分别按在View1与View2之上时,因为两个触控点都落在 ViewGroup之内,因此 ViewGroup
会收到一条双点的事件序列,当 ViewGroup将事件派发给View1和View2时,就必须将其拆
分为两个单点序列,并分别派发给vew1和vew2。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第23张图片
     之所以讨论输入事件序列的概念,是由于 Android在派发触摸事件时有一个很重要的
原则,就是事件序列的不可中断性,即一旦一个控件决定了接受一个触控点的 ACTION_DOWN
或 ACTION_POINTER_DOWN事件(在事件处理函数中返回true),表示控件将接受
序列的所有后续事件,即便触控点移动到控件区域之外也是如此。
理解事件序列的概念对在
后续代码分析过程中把握这一原则十分重要。

      触摸事件的序列除 ACTION_UP以外还有一种结来标志一一 ACTION_CANCEL。比
如,一个正在接受事件序列的控件从控件树中被移除,或者发生了 Activity切换等
那么它将收到 ACTION_CANCEL而不是 ACTION_UP,此时控件需要中断对事件的
处理并将自己恢复到接受事件序列之前的状态。

2.控件对触摸事件的接收与处理

      回过头来继续代码的讨论, ViewRootlmpl将触摸事件交给了根控件的 View.dispatchPointerEvent(),
View.dispatchPointerEvent()又将事件交给了View.dispatchTouchEvent()
同View.dispatchKeyEven()一样,
View.dispatchTouchEvent()拥有 ViewGroup以及View两种实现。类比
可知 ViewGroup的实现负责将触摸事件沿着控件树向子控件进行派发,而View的实现则主要
用于事件接收与处理工作。

首先分析相对简单的vView实现。
[View. java-->View.dispatchTouchEvent()]
public boolean dispatchTouchEvent(MotionEvent event){
    ......
    /*①首先触摸事件必须经过onFilterTouchEventForSecurity()过滤。出于对最终用户的信息安全
      角度的考虑,当本窗口位于另外一个非全屏窗口之下时,可能会阻止控件处理触摸事件
*/
    if(onFilterTouchEventForSecurity(event)){
        // ②尝试让此控件的 OnTouchlietener处理触摸事件
        Listenerinto li = mListenerinfo;
        if (li != null && li.mOnTouchlistener != null
            && (mViewFlags & ENABLED_MASK)== ENABLED
            && li.mOnTouchListener.onTouch(this,event)){
            return true;
        }
        // ③倘若OnTouchListener对事件不感兴趣,则尝试令onTouchEvent()回调处理事件
        if (onTouchEvent(event)){
            return true;
        }
    }
    ......
    return false;
}
      可见 View.dispatchTouchEvent()与View.dispatchKeyEvent()的实现如出一辙,用于使
OnTouchListener或 onTouchEvent()进行事件的处理。区别在于它多了一道由
onFilterTouchEventForSecurity()完成的验证程序。这道验证程序是检查触摸事件是否带有
FLAG_WINDOW_IS_OBSCURED标记
(这一标记由 InputDispatcher设置,依据是派发目标窗口在
WMS中 WindowState.mObscured成员的取值
,参考本书第4章),以及控件的 mViewFlags中
是否存在 FILTER_TOUCHES_WHEN_OBSCURED标记,当两者同时存在时,将会阻止事件
被此控件处理。
事件中存在 FLAG_WINDOW_IS_OBSCURED标记表明此窗口部分或完整地
被另外一个窗口所遮挡,
此时用户有可能因为无法看到当前窗口的一些敏感信息或被遮挡窗
口的恶意信息所蒙骗而进行一些不安全的操作,如图6-28所示。因此, Android提供了这一机
制避免用户的点击事件得到响应从而降低安全风险。开发者可以通过在执行敏感行为的控件
上调用 View.setFilterTouchesWhenObscured()方法在 mViewFlags中添加 FILTER_TOUCHES_
WHEN_OBSCURED
标记,以便在这个控件上启用这一机制。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第24张图片

3. ViewGroup对触摸事件的派发
      接下来讨论 dispatchTouchEvent()的 ViewGroup版本的实现。 ViewGroup的主要工作是将
触摸事件派发给合适的子控件。在本节中,读者将会看到 ViewGraup对前面所介绍的知识的
具体运用,以及对事件序列不可中断性原则的坚持。
      事件的派发分为确定派发目标执行派发两个工作。
      对触摸事件来说,根据序列不可中断性原则,确定派发目标发生在收到 ACTION_DOWN
或 ACTION_POINTER_DOWN的时刻。此时 ViewGroup会按照逆绘制顺序依次查找事件坐
标所落在的子控件,并将事件发送给子控件的 dispatchTouchEvent()方法,然后根据返回值确
定第一个愿意接受这一序列的子控件,将其确定后续事件为派发目标。一旦通过 ACTION_DOWN
或 ACTION_POINTER_DOWN确定派发目标, ViewGroup会将此触控点的与
目标建立绑定关系,属于此触控点的事件序列都会发送给这一目标。 ViewGroup通过一个
TouchTarget类的实例来描述这一个绑定,这个类保存了一个触控点ID的列表以及一个view
实例,以此实现从触控点ID到目标控件的映射。

      执行派发的工作主要是将事件传递给目标的 dispatchTouchEvent()方法。由于多点触控
的存在,执行派发时可能需要将 MotionEvent进行拆分,将 ViewGroup所收到的事件序列
拆分成多个子序列并发送给多个子控件。
因此不难理解ViewGroup在其派发过程中可能维
护着多个 TouchTarget实例。 TouchTarget与 ViewRootlmpl的 QueuedInputEvent类一样存在
着一个next成员,即它是一个单向链表。 ViewGroup将所有的 TouchTarget存储在一个以
mFirstTouchTarget为表头的单向链表中。

      另外在ViewGroup.dispatchTouchEvent()中会通过 onlnterceptTouchEvent()尝试对输入事件
进行截获。
为了减少复杂度,本节不会对事件的截取进行讨论。
本节将按照上述的两个工作对 ViewGroup.dispatchTouchEvent()进行讨论

(1)派发目标的确定

参考 ViewGroup.dispatchTouchEvent()的代码
[ViewGroup.java--> ViewGroup.dispatchTouchEvent()]
public boolean dispatchTouchEvent(MotionEvent ev) {
    .......
    boolean handled = false;
    //同样, ViewGroup也会对遮盖状态进行检查与过滤
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        //剔除触控点的索引号以获取实际的动作
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            /*① ACTION_DOWN意味一条崭新的事件序列的开始。此时ViewGroup会重置所有与触摸事件
              派发相关的状态,包括清空 TouchTarcet列表,这样一来 ViewGroup便准备好进行一次崭新的
              触摸事件派发工作了
*/
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
        
        ......... //检查此 ViewGroup是否需要对事件进行截取,暂不考虑截取的情况
        /*canceled表示 ViewGroup所收到的这一事件序列是否被取消(由于被移出控件树或发生了 Activity切换等),
          为了筒单起见,本节忽略被取消的情况*
/
        final boolean canceled = resetCanceINextUpFlag(this)
                            || actionMasked == MotionEvent.ACTION_CANCEL;
        /*split表示此控件树是否启用前述的事件序列的拆分机制,开发者可以通过
          setMotionEventsplittingEnabled()方法启用或禁用这一机制
*/
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS)!=0;
        /*如果此次事件产生了新的派发目标(仅发生于 ACTION_DOWN或 ACTION_ POINTER_DOWN)
          那么新的 TouchTarge实例将保存在这里
*/
        TouchTarget newTouchTarget = null;
        /*由于确定派发目标使用了子控件的 dispatchTouchEvent(),因此当确定派发目标之后这一事件
          实际上已经完成发了。这种情况下此变量将会被设置为true,以跳过后续的派发过程
*/
        boolean alreadyDispatchedToNewTouchTarget = false:
        //倘若事件序列没有被取消,也没有被当前 ViewGroup所截取,才有进行派发目标查找的必要
        if (! canceled && ! intercepted) {
            /*如果事件的实际动作是 ACTION_DOWN或者 ACTION_POINTER_DOWN,
            标志着一个子序列的开始,此时需要进行派发目标的确定
*/
            if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE){
                //获取这一按下事件的触控点的索引号
                final int actionIndex = ev.getActionIndex();
                /*②通过索引号获取触控点的ID。为了能够在一个整型变量中存储一个ID列表,这里通过
                将1进行左移若干个位的方式将ID转换为2的ID次方的形式并存储在 idBitsToAssign变量中。
                当找到一个派发目标之后,会将这个 idBitsToAssign添加到派发目标所对应的TouchTarget中,
                从而使得这一触控点被綁定在 TouchTarget上。由此可见, Android控件系统最多可以支持32个触控点。 
                注意,当split为false及 ViewGroup没有启用序列的拆分时, idBitsToAssign被设
                置为 TouchTarget.ALL_POINTER_IDS,意思是所有触控点的事件都会被派发给后续被
                确定的目标控件
*/
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex):
                                        TouchTarget.ALL_POINTER_IDS;
                ......
                /*接下来就要开始对子控件按照逆绘制顺序进行遍历,
                检查哪一个控件对这一新的事件子序列感兴趣
*/
                final int children Count mChildrencour
                if (childrencount !=0 ){
                    final view[] children =mChildren;
                    /*由于触摸事件是基于位置进行派发目标的查找,因此必须获取事件的坐标。
                    注意这里通过触控点的索引号获取坐标
*/
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 接照逆绘制顺序的遍历循环,注意顺序是从childrecount-1开始到0
                    final boolean customOrder = ischildrenDrawingOrderEnabled();
                    for (int i = childrencount -1: i >=0; i--){
                        final int childIndex = customorder?getChildDrawingOrder(childrencount, i):i;
                        final View child = children[childIndex];
                        //③首先检查事件坐标是否落在控件之内。如果没有位于控伴内则继续查找下个控件
                        if(! canViewReceivepointerEvents(child)
                                || !isTranaformecTouchPointInview(x, y, child, null)){
                            continue;
                        }
                        /*④从mFirstTouchTarge列表中查找控件所对应的 TouchTarget。若
                        子控件所对应的 TouchTarget已经存在,表明此控件已经在接收另外一个事件子序
                        列, ViewGroup会默认此控件对这一条子序列也感兴趣。
此时将触控点ID绑定
                        在其上,并终止派发目标的查找。
                        后续的派发工作会将此事件发给这一控件
*/
                        newTouchTarget = getTouchTarget(child);
                        if (new TouchTarget != null){
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        .......
                        /*⑤使用 dispatchTransformedTouchEvent()方法尝试将事件派发给当前子
                        控件。
此方法主要包含4个工作
                            1>根据最后一个参数 idBitsToAssign将其指定的触控点的信息从原始事件
                                 ev中分离出来并产生一个新的 MotionEvent。
                            2>如果有必要,修改新 MotionEvent的Action。
                            3>把事件的坐标转换到子控件的坐标系下。
                            4>将新的 MotionEvent派发绐子控件。
                        此方法的返回值确定了子控件是否决定接受这一事件序列。
稍后会详细讨论此方法
                        买现*/
                        if (dispatchTransformedTouchEvent(ev,false, child, idBitsToAssign)){
                            .......
                            /*当子控件决定接受这一事件时,为其创建一个TouchTarget并保存在
                            mFirstTouchTarget链表中,从此来自此触控点的事件都会派发给这个子控件
*/
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            /*如前文所述,此事件已经在子控件中得到了处理。因此标记如下变量为true,
                            后续的事件派发流程将不会再次发送此事件到这一子控件
*/
                            alreadyDispatchedroNewTouchTarget = true;
                            break;
                        }
                    }
                }
                /*⑥在上述的遍历过程中没能找到一个合适的子控件以接受这一事件序列的情况下,
                ViewGroup会将这一事件序列强行交给最近一次接受事件序列的子控件。

                这看似不讲理的做法有它的实际意义。因为用户一根手指按下时,其意图往往是为了配合
                上一根按下的手指以进行多点操作。
因此,这是ViewGroup猜测用户使用习惯的一种策略*/
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;
                    while(newTouchTarget.next != null){
                        newTouchTarget = newTouchTarget.next;
                    }
                    //将事件序列的触控点ID绑定到控件的 TouchTarget中
                    newTouchTarget.pointerIdBits |=idBitsToAssign;
                }
            }
        }
        //实际派发工作的代码
        ......
    }
}
      简单来说, ViewGroup.dispatchEven()方法前半部分的目的是以更新 mFirstTouchTarget
列表的方式确定一系列的派发目标。每个派发目标由 TouchTarget表示,并在 TouchTarget
中的 pointerldBits中保管目标所感兴趣的触控点的列表。
这一信息是后续进行实际派发
的关键依据。ViewGroup确定派发目标的原则如下:
      口  仅当事件的动作为 ACTION_DOWN或 ACTION_POINTER_DOWN时才会进行派发目标
            的查找。因为这些动作标志着新的事件子序列的开始, ViewGroup仅需要对新的序列
            查找一个派发目标

      口  ViewGroup会沿着绘制顺序相反的方向,即从上到下进行查找。毕竞用户往往都是希
            望点击在他能看得到的东西上。因此 Z-Order越靠上的控件拥有越高的接受事件的优
            先级。

            ViewGroup会将一个控件作为派发目标候选的先决条件是事件的坐标位于其边界内
            部。当事件落入子控件内部并且它接受过另外一条事件序列时,则直接认定它就是此
            事件序列的派发目标。因为当用户将手指按在一个控件上,再把另一根手指也按在其
            上时,很可能是他想对此控件进行多点操作.

      口  ViewGroup会首先通过 dispatchTransformedTouchEven()尝试将事件派发给候选控件,
            倘若控件在其事件处理函数中返回true,则可以确定它就是派发目标,否则继续测试
            下一个子控件。

      口  当遍历了所有子控件后都无法找到一个合适的派发目标时(事件落在了所有子控件之
            外,或者所有子控件对此事件都不感兴趣), ViewGroup会强行将接受了上一条事件序
            列的子控件作为派发目标。因为 ViewGroup猜测用户以相邻次序按下的两根手指应该
            包含着能够共同完成某种任务的期望。可以看出, ViewGroup尽其所能地将事件派发
            给子控件,而不是将事件留给自己处理。不过当目前没有任何一个子控件正在接受事
            件序列时( mTouchTarget为null)., ViewGroup便不得不将事件交给自己处理了。

(2)依据 TouchTarget进行触摸事件的派发

      接下来是实际的派发工作。不难想到派发工作是围绕着 mFirstTouchTarget列表完成的。
ViewGroup.dispatchTouchEven()方法会遍历列表中的每一个TouchTarget,从MotionEvent
中提取 TouchTarget所感兴趣的触控点的信息并组成新的 MotionEvent,然后将其派发给
TouchTarget所代表的子控件。

参考 dispatch Touch Evento实现:
[ViewGroup.java-->ViewGroup.dispatchTouchEvent()]
public boolean dispatchTouchEvent( MotionEvent ev) {
    ......
    boolean handled = false;
    //当然,实际的派发工作也位于安全检查之内
    if (onFilterTouchEventForSecurity(ev)){
        .......//查找派发目标的代码
        if(mFirstTouchTarget != null){
            /*①当mFirstTouchTarget为null时,表明之前未能找到任何一个合适的子控件接受事件
              序列,此时只能由 ViewGroup自己处理输入事件。注意将事件派发给 ViewGroup自已也使
              用了dispatchTransformedTouchEvent()方法,不过会将child参数设置为null
*/
            handled dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
        }else {
            //遍历 mFirstTouchTarget链表,为每一个 TouchTarget派发事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null){
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
                    /*②倘若 TouchTarget是新确定的 TouchTarget,那么在确定的过程中目标控件
                    已经完成了事件处理,因此不需要再派发
*/
                    handled = true;
                } else {
                    /*cancelchild表示因为某种原因需要中断目标控件继续接受事件序列,这往住由于
                    目标控件即将被移出控件树,或者 ViewGroup决定截取此事件序列。此时仍然会
                    将事件发送给目标控件,但是其动作会被改成 ACTION_CANCEL
*/
                    final boolean cancelChild = resetcancelNextUpFlag(target.child)|| intercepted;
                    //③使用dispatchTransformedTouchEvent()方法派发事件给目标控件
                    if(dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)){
                        handled = true;
                    }
                    /*倘若决定终止目标控件继续接受事件序列,则将其对应的 TouchTarget从链表中删除,
                    并回收。下次事件到来时将不会为其进行事件派发
*/
                    if (cancelchild){
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else{
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        ......
    }
}

      派发过程的实现思路十分清晰。即倘若没能找到合适的派发目标,则将事件派发给
ViewGroup自己。否则遍历每个 TouchTarget并将事件派发给它。
      在这里再一次看到了 ViewGroup.dispatchTransformedTouchEvent()方法。可见它是将事件
派发给子控件的必由之路
,因此它非常重要。参考实现如下:
[View Group. java-->ViewGroup.dispatchTransformedTouchEvent()]
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                            View child, int desiredPointerIdBits) {
    final boolean handled;
    /*①首先处理   当需要终止子控件对事件序列进行处  的情况。此时仅需要将事件的动作换为 ACTION_CANCEL
    并调用子控件的 dispatchTouchEvent()即可。此时并没有进行上节所述的如坐标变换等动作,
    因为ACTION_CANCEL仅仅是一个要求接受者立刻终止事件处理并恢复到事件处理之前状态的一个记号
    而已,
此时其所携带的其他信息如坐标等都是没有意义的
*/
    final int oldAction = event.getAction();
    if(cancel || oldAction == MotionEvent.ACTION_CANCEL){
        event.setAction(MotionEvent ACTION_CANCEL);
        if (child = null){
            /*当child参数为null时表示事件需要派发给 ViewGroup自己。
            注意使是前文所讨论过的View版本的实现
*/
            handled= super.dispatchTouchEvent(event);
        }else{
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        //事件派发完成,直接返回
        return handled;

    }
    /*这两个局部变量是确定是否需要进行事件序列分割的依据。oldPointerIdBits表示了原始事件中所有
    触控点的列表。而 newPointerldBits则表示了目标希望接受的触控点的列表,它是 oldpoineridBits
    的一个子集

    既然 desiredpointeldBits参数已经摧述了目标希望接收的触控点,为什么 newPointerldBits是必要
    的呢?因为 desiredpointerIdBits的值有可能是 TouchTarget.ALL_ POINTER工Ds此时它并不
    能准确地表示实际要派发的触控点列表
*/
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointeridBits = oldPointeridBits & desiredPointeridBits;
    ......
    /* tranformedEvent是一个来自原始 MotionEvent的新的 MotionEvent,它只包含了目标所感兴趣的
    触控点,
派发给目标事件对象是它而不是原始事件*/
    final MotionEvent transformedevent;
    /*②生成 transformedEvent。比较 newPointerIdBits以及 oldPointerIdBits,如果二者相等
    则表示目标对原始事件的所有触控点全盘接受,因此 transformedEvent仅仅是原始事件的一个复制
    而当二者不相等时, transformedevent是原始事件的一个子集,此时需要使用 MotionEvent.split()
    方法将这一子集分离出来以构成 transformedEvent
*/
    if (newPointerIdBits == oldPointerIdBits) {
        /*当目标控件不存在通过 setScaleX()等方法进行的变换时,为了效率会将原始事件简单地进行控件
        位置与滚动量变换之后发送给目标的 dispatchTouchEvent()方法并返回。

        因为这一计算在后面的代码中仍有体现,因此为了篇幅省略了这部分代码*/
        ......
        trans formedEvent= MotionEvent.obtain( event);//复制原始事件
    }else{
        transformedevent= event.split( newpointerldeits);//分离原始事件中的一个子集
    }
    //③对 transformedEvent进行坐标系变换,并发送给目标
    if (child == null) {
        //与之前一样,当chi1d参数为null时表示将事件发送给 viewGroup自己
        handled super.dispatchTouchEvent(trans formedEvent);

    }else{
        //对 transformedevent进行坐标系变换,使之位于派发的坐标系之中
        //首先是计算 ViewGroup的滚动量以及目标控件的位置

        final float offsetX = mScrollx - child.mLeft;
        final float offsetY = mscrolly - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);

        /*然后当目标控件中存在使用 setScaleX()等方法设置的炬阵变挑时,将对事件坐标进行变换。此次
        变换完成之后,事件坐标点便位于目标控件的坐标系了*
        if (! child.hasIdentityMatrix(){
            transformedEvent.transform(child.getInverseMatrix());
        }

        //通过 dispatchTouchEvent()发送给目标控件
        handled = child.dispatchTouchEvent (transformedEvent);

    }
    //销毁transformedEvent
    transformedEvent.recycle();
    return handled;
}
抛开其中用于处理 ACTION_CANCEL的代码,不难总结出此方法的三个步骤:
      口  生成 transformedEvent,根据目标所感兴趣的触控点列表, transformedEvent有可能是
            原始事件的一个副本,或者仅包含部分触控点信息的一个子集
      口  对 transformedEvent进行坐标系变换,使之位于目标控件的坐标系之中
      口  通过 dispatchTouchEvent()将 transformedEvent发送给目标控件。
当然,将 transformedEvent进行销毁也是必需的一个步骤,不过它并不算是核心步骤

虽然仅仅包含三个步骤,但是上一节却介绍此方法完成了4项工作。不错,修改事件的
Action并没有直接地出现在这一方法之中。事实上,这一工作发生在 MotionEvent.split()方法
之中。
在分析 MotionEvent.split()方法如何修改事件的 Action之前,首先讨论一下为什么要修
改事件的 Action。

      在讨论事件序列的概念时曾经提到过它一定是以 ACTION_DOWN开始,经历一系列的
ACTION_POINTER_DOWN/ACTION_POINTER_UP/ACTION_MOVE之后以一条 ACTION_
UP结束。
假设一个 ViewGroup收到如图6-26所示的一条事件序列,而它的一个子控件仅对
触控点3对应的子序列感兴趣,于是,此 ViewGroup通过 MotionEvent.split()方法将触控点3
的信息分离出来构成新的 transformedEvent,并派发给子控件。
问题在于,触控点3的子序列
起始事件的动作为 ACTION_POINTER_DOWN,并且终止事件的动作为 ACTION_POINTER
_UP,这对 ViewGroup来说是合理的,因为它是一条子序列。但是它在目标子控件看来就
不合理了,因为这是子控件的一条完整的事件序列,而且根据事件序列的性质,要求其以
ACTION_DOWN开始并以 ACTION_UP结束。因此,在为子控件分离触控点3的起始与终止
事件时,必须修改事件的动作为 ACTION_DOWN以及 ACTION_UP,以保证子控件能够正常
工作。这种情况的需求是将 ACTION_POINTER_DOWN/UP修改为 ACTION_DOWN/UP

图6-29所示。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第25张图片

      还有第二种情况,仍假设一个ViewGroup
收到如图6-26所示的一个事件序列,而它的
个子控件仅对触控点2对应的子序列感兴
趣。那么当触控点3的按下事件发生时,原
始事件的动作是 ACTION_POINTER_DOWN,
很明显子控件对这一按下动作是不感兴趣的,
但是它却感兴趣原始事件中所携带的触控点2的坐标等信息。因此当 ViewGroup为子控件分
离触控点2的信息到一个 transformedEvent时,
需要将事件的动作修改为 ACTION_MOVE。
而触控点3的抬起事件到来时亦然。这种情况
的需求是将 ACTION_POINTER_DOWN/UP修
改为 ACTION_MOVE
,如图6-30所示。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第26张图片

      还有最后一种稍微复杂一些的情况。仍
以图6-26为例,如果子控件同时对触控点
2与触控点3感兴趣。那么当触控点3按下
时,子控件确实需要一个ACTION_POINTER_DOWN事件。此时看似不需要进行事件动作
的修改,其实不然。如前文所述,事件的动作为 ACTION_POINTER_DOWN/UP时,其高
位存储了触控点的索引(即触控点信息在 mSamplePointerCoords数组中的位置),触控点的
索引也是事件动作的一部分。
事实上,由于通过 MotionEvent.split()分离出来的 MotionEvent
使用了自己的 mSamplePointerCoords
数组
,所以对本例的子控件来说,为
其分离出的用于描述触控点3按下
ACTION_POINTER_DOWN对应的触
控点索引   相对于  原始事件已经发生了变
化。这种情况的需求是修改 ACTION_
POINTER_DOWN/UP所对应的触控点
索引
,如图6-31所示。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第27张图片

      了解了修改事件动作的原因,那么
分析 MotionEvent.split()如何修改事件动作就简单了。
      口  首先,修改事件动作的情况仅发生在原始事件的动作为 ACTION_POINTER_DOWN
            或 ACTION_POINTER_UP的情况下。

      口  当传递给 MotionEvent.split()的触控点ID列表中仅包含一个触控点,并且它是引
            发 ACTION_POINTER_DOWN/UP的触控点时,分离出的事件的动作将会被设置为
            ACTION_DOWN/UP。对应上述第一种情况。

      口  当传递给 MotionEvent.split()的触控点ID列表中不包含引发 ACTION_POINTER_
            DOWN/UP的触控点时,则表示不关心这一按下或抬起动作,分离出的事件的动作将
            会被设置为 ACTION_MOVE。对应上述第二种情况。

      口  当传递给 Motion Event.split()的触控点ID列表中包含多个触控点,并且其中之一是
            引发 ACTION_POINTER_DOWN/UP的触控点时,分离出的事件动作将会被保持为
            ACTION_POINTER_DOWN/UP,但是其包含的触控点索引将会根据新事件内部的
            mSampleCoords数组的状况重新计算。

修改事件动作的代码在这里就不详细讨论了,读者可以根据上述内容自行研究。
      在dispatchTransformedTouchEvent()方法中,当child参数为null时,表示派发对象
是 ViewGroup本身,此时它通过 super.dispatchTouchEven()方法转而调用其View版本的
实现,从而使 ViewGroup的 onTouch()以及 OnTouchListener得以对事件进行处理。
而当
child参数不为nul时,则会调用 child.dispatchTouchEvent()。如果 child是一个 ViewGroup
那么自然而然, child又会按照本节所讨论的派发过程,先确定其派发目标,然后再通过
dispatchTransformedTouchEvent()将事件派发给 child的子控件,如此递归下去。

(3)移除派发目标

      ViewGroup.dispatchTouchEvent()先确定派发目标控件,为其创建 TouchTarget并追加
到 mFirstTouchTarget链表中,然后依照这一链表依次将事件派发给对应的目标控件。那么
什么时候会将 TouchTarget从链表中移除呢?显然,当目标控件所感兴趣的最后一条事件序
列结束时,或者 ViewGroup收到一个 ACTION_CANCEL或 ACTION_UP时,就是将其从
mFirstTouchTarget链表中移除的时机了
参考 ViewGroup.dispatchTouchEvent()方法最后一部分
代码:
[ Group java-->ViewGroup.dispatchTouchEvent()]
public boolean dispatchTouchEvent (Motion Event ev){
    ......
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)){
        ......//查找派发目标的代码
        ......//进行实际派发工作的代码

        if(canceled
                || actionMasked = MotionEvent.ACTION_UP
                || actionMasked = MotionEvent.ACTION_HOVER_MOVE){
            /*当 ViewGroup收到一个ACTION_CANCEL事件或 ACTION_UP事件时,
              整个事件序列已经结束,因此删除所有TouchTarget
*/
            resetTouchstate();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP){
            /*当事件是一个 ACTION_POINTER_UP时,将其对应的触控点ID从对应的 TouchTarget中
            移除。 removePointersFromTouchTargets()会在 TouchTarget的最后一个触控点ID
            移除的同时,将这个 TouchTarget从 mFirstTouchTarget链表中删除并销毁
*/
            final int actionIndex ev. getActionIndex();
            final int idBitsToRemove = 1 << ev getPointerId(actionIndex);
            removePointersFromTouchTargets (idBitsToRemove);
        }
    }
    ......
    return handled;
}

4.触摸事件派发的总结

      相对于按键事件,触摸事件的派发过程与策略要复杂得多。其复杂的根本原因是多点触
摸与事件序列拆分机制的存在。理解这一机制的来龙去脉之后就可以发现触摸事件的派发过
程清晰了很多:其本质与按键事件派发一样,经历了从根控件开始在子控件中寻找目标并发
送给目标子控件,然后目标子控件在其子控件中寻找目标再发送给子控件的子控件,如此递
归下去直到事件得到最终处理。
      而多点触摸与事件序列拆分是围绕着 TouchTarget这一核心完成的。 TouchTarget是绑定触
控点与目标控件的纽带。每当一个事件子序列到来时, ViewGroup都会新建或选择一个现有的
TouchTarget,并将子序列对应的触控点D绑定在其上,从而生成 TouchTarget感兴趣的触控
点列表。随后的事件到来时 ViewGroup会从其所收到的 MotionEvent中为每一个 TouchTarget
分离出包含它所感兴趣的触控点的新 MotionEvent,然后派发给对应的子控件。 TouchTarget的
数量就事件序列经过此 ViewGroup后拆分成的子序列的个数。
      至此,关于触摸事件的派发的讨论便告一段落。

6.5.6输入事件派发的总结

      本节继续第5章的讨论,介绍从 ViewRootlmpl通过 InputEventReceiver接收输入事件到
目标控件对事件进行处理的完成过程。其中主要讨论了如下几个内容
      口  触摸模式。当系统处于触摸模式时,表示用户主要通过触摸的方式对系统进行输入操
            作,此时部分控件将无法获取焦点。
      口  焦点。焦点是按键事件派发的唯一依据,同时它也通过 DrawableState机制影响了控
            件的表现方式。
      口  按键事件派发。按键事件从根控件开始沿着 mFocused链表派发给最终拥有焦点的控
            件。在按键事件正式派发给目标控件之前,它会先到输入法中走一遭。不过控件仍然
            可以通过重写 onKeyPrelme()方法在输入法之前获得处理事件的机会
      口  触摸事件的派发。触摸事件可能包含着多个触控点的信息。 ViewRootlmpl收到一个
            触摸事件后并在控件树中派发的过程里可能会被拆分成多个触摸事件,沿着不同的
            路径到达多个目标控件。其拆分的依据是 ViewGroup中 TouchTarget的列表,而一
            个 TouchTarget可以理解为触摸事件的一条派发路径,因此 ViewGroup中有多少个
            TouchTarget,触摸事件机会被拆分成几份。
      本节仅在控件系统的范畴中对事件的派发进行讨论,因此并没有讨论 Activity、 Dialog等
因素存在时所产生的影响。 Activity, Dialog作为控件系统的一个用户同样遵从本节所讨论的
派发原理,只不过它们的控件树的根控件 DecorView的特殊性使得输入事件的派发有了一些
新的内容。
接下来的6.6节将会讨论 Activity与 Dialog对控件系统的使用,其中会涉及它们对
输入事件派发所产生的影响。

6.6 Activity与控件系统

      此前的内容都是在 WindowManager、 ViewRootlmpl与控件树三者所组成的体系之下进行
讨论的。在绝大多数情况下,窗口以及控件树往往存在于一些更加高级别的概念中。这些高
级别的概念即 Activity以及 Dialogo本节将会讨论在 Activity及 Dialog之下的窗口以及控件树
的特性。

6.6.1理解 PhoneWindow

      在正式讨论 Activity与 Dialog相关的话题之前,本节先介绍一个新的窗口的概念,它是
com. view.Window,一个抽象类,它从更高级别的层次上描述了一个窗口的特性。
      当开发者通过 WindowManager、 LayoutParams以及控件树创建一个窗口时,需要手动初
始化 LayoutParams以及自行构建控件树。虽说这种方式已经比直接使用WMS、 WIndow与
Surface这些更加底层的方式进步了不少,但是仍显繁琐。现今窗口中的内容往往都有不成文
的规范,如在指定的位置有标题栏、动作栏、图标等,手动创建符合这些规范的控件树绝不
是一件令人愉快的事情。为此 Android提供了 com.view.Window(后文中在不产生歧义的情况
下直接称其为 Window)类以在更高的级别上操作窗口。

      那么 Window类究竞负责做些什么呢? Window类中有三个最核心的组件; WindowManager.
LayoutParams、一棵控件树以及 WindowCallback。因此它的作用也要从这三个组件
分别说起。
      针对 WindowManager.LayoutParams, Window类提供了一系列set方法用于设置 LayoutParams
属性。
这项工作看似没什么用处,还不如开发者自行管理 LayoutParams来得方便。不过
Window类的优势在于它可以根据用例初始化 LayoutParams中的属性,例如窗口令牌、窗口名
称以及FLAG等。
      针对其所包含的那一棵控件树, Window为使用者提供了多种多样的控件树模板。这些模
板可以为窗口提供形式多样却又风格统一的展示方式以及辅助功能,例如标题栏、图标、顶
部进度条、动作栏等,甚至它还为使用者提供了选项菜单的实现。
使用者仅需将显示其所关
心内容的控件树交给它,它就会将其嵌套在模板的合适位置。这一模板就是最终显示时的窗
口外观。(为了叙述时更好理解,后文中将这一控件树模板称为外观模板。) Window类提供
了接口用于模板选择、指定期望显示的内容以及修改模板的属性(如标题、图标、进度条进
度等)。
      Window.Callback是一个接口, Window的使用者可以实现这个接口并注册到 Window中
于是每当窗口中发生变化时都可以得到通知。可以通过这一接口得到的通知内容有输入事件、
窗口属性的变更、菜单的弹出/选择等。

      简单来说, Window类是一个模板,它大大简化了一个符合使用习惯的控件树的创建过程
使得使用者仅需要关注控件树中其真正感兴趣的部分,并且仅需少量的工作就可以使这部分
嵌套在一个漂亮而专业的窗口外观之下,而不用关心这一窗口外观的控件树的构成。另外由
于 Window类是一个抽象类,因此使用不同的 Window类的实现时还可以在不修改应用程序原
有逻辑的情况下提供完全不同的窗口外观。
      目前 Android中使用的 Window类的实现是 PhoneWindow。Window类中提供了用于修改
LayoutParams的接口等通用功能实现,而 PhoneWindow类则负责具体的外观模板的实现。

      PhoneWindow与 PhoneWindowManage之间没有任何关系。PhoneWindowManager是
WMS的一个组成部分,用于提供与窗口管理相关的策略。而 PhoneWindow是一个用
于快速构建窗口外观的工具类,相较之下 PhoneWindow是一个更接近于控件系统的
概念。
而这唯一有关系的地方是它们都是由 com. android. internal policy. impl. Policy类中的工厂
方法创建的。从字面意义上来看, Google最初应该是希望在手机上使用 PhoneWindow
的实现,而在其他设备(如平板电脑)上使用其他的实现(如 TabletWindow)以提供
更加差异化的窗口外观,只不过这一想法没有最终实施。

本节将着重讨论 PhoneWindow类如何为窗口创建窗口外观。

1.选择窗口外观与设置显示内容

      只要读者开发过任何一个包含 Activity的 Android程序,相信一定会对 Activity. requestWindowFeature()
以及 Activity. setContentView()两个方法十分熟悉。requestWindowFeature()负责指定 Activity窗
的特性,如是否拥有标题栏,是否存在一个进度条,程序图标的位置等。
换言之, Activity.
requestWindowFeature()方法决定了窗口的外观模板。setContentView()则设置一棵控件树用于显示在
Activity中。
在了解了 Window的作用之后应该能猜测到 Activity使用了 Window类,并且这
两个方法的工作应该是在 Window类中完成的。
而事实正是如此,这两个方法的实现如下:
[Activity java-->Activity.requestWindowFeature()]
public final boolean requesthWindowFeature(int featureId){
    //直接将请求转发给 window
    return getwindow().requestFeature(featureId);

}
[Activity, java-->Activity.setContentView()]
public void setContentview (int layoutResID){
    //直接将请求转发给 Window
    getwindow().setContentWiew(layoutResID);
    initActionBar();

}
      Activity何时创建了 Window类的实例留待6.6.2节进行讨论。本节只讨论这两个方法如
何影响 Window为 Activity创建控件树。由于 Android使用了 PhoneWindow作为 Window的实
现,因此分析的重点在 PhoneWindow对这两个方法的实现上。
首先是 PhoneWindow.requestFeature()。参考其实现:
[Phone Window. java-->PhoneWindow.requestFeature()]
public boolean requestFeature(int featureId){
    /*倘若mContentParent不为null时,调用了 requestFeature方法则会抛出一个运行时异常
    mContentParent是一个 ViewGroup,是用户通过 setContentView()设置的控件树的直接父控件。
    当它不为null时,表示外观模板已经建立,那么此时再进行 requesteearure()操作为时已晚
*/
    if (mContentParent != null) {
        throw new AndroidRuntimeException("......");
    }
    /*接下来是对 feature进行相容性检查。因为 PhonenWindow允许使用者设置多个 feature,而不同的 feature
    之问可能存在互斥性,例如,当要求窗口外观不存在标题栏时,就不再允许窗口带有动作条,因为动作条
    是标题栏的一部分
*/
    .......
    //最后调用 Window类的 requestFeature()实现完成 feature的最终设置
    return super.requestFeature(featureId);
}
再看 Window.requestFeature()方法:
[Window. java->Window.requestFeature()]
public boolean requestFeature(int featureId){
    //与处理触控点ID的方式类似, Window以bit的方式存储 feature列表
    final int flag = 1<     //将 feature添加到 mFeatures成员之中
    mFeatures != flag;

    ......
}
      可见设置窗口特性的工作是十分简单的。请记住窗口的特性被保存在 Window.mFeatures
成员之中。 requestFeature()方法并没有立刻创建外观模板,但是 mFeatures成员将会为创建外
观模板提供依据。
接下来分析 PhoneWindow.setContentView()方法。
[ PhoneWindow. java-->PhoneWindow.setContentView()]
public void setContentview(int layoutResID) {
    //①首先是为窗口准备外观模板
    if (mContentParent == null) {
        /*当 mContentParent为null时,表明外观模板尚未创建,此时会通过 installDecor()方法创建
        个外观模板。创建完成之后 mContentParant便会被设置模板中的一个ViewGroup并且随后它会
        作为使用者提供的控件树的父控件
*/
        installDecor();
    } else {
        /*倘若外观模板已经创建,则清空 mContentParent的子控件,使其准备好作为新的控件树的父控件*/
        mContentParent.removeAllViews();
    }
    /*②将使用者给定的layout实例化为一棵控件树,然后作为子控件保存在 mContentParent之中。
    完成这个操作之后, PhoneWindaw便完成了整棵控件树的创建
*/
    mLayoutInflater.inflate(layoutResID, mContentparent);
    /*③Callback接口被 Window用来向使用者通知其内部所发生的变化。此时通知使用者 Window的控件树发
    生了改变。作为 Window的使用者, Activity类实现了这一接口,因此开发者可以通过重写 Activity的
    这一方法从而对这些变化做出反应
*/
    final callback cb= getcallback();
    if (cb != null && !isDestroyed(){
        cb.onContentChanged();
    }
}
      可见, Window控件树的创建是在 Window的setContentView()方法中完成的。其创建过
程分为创建外观模板以及实例化使用者提供的控件树并添加到模板中两个步骤。在完成控件
树的创建之后 Window会通过 Callback接口将这一变化通知给其使用者,例如 Activity。

      实例化使用者所提供的控件树并没有什么可讨论的地方。本节更关心的是PhoneWindow
如何创建外观模板,因此有必要讨论一下 Phone window. installDecor()的实现。参考其代码:
[Phone Window. java-->PhoneWindow.installDecor()]
private void installDecor(){
    //①首先创建控件树的根控件,并保存在mDecor成员之中
    if (mDecor == null){
        //使用 generateDecor()方法创建根控件
        mDecor = generateDecor();
        //设置根控件的焦点优先级为子控件优先
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        /*设置 mDecor为 RootNameSpace在6.5.2节介绍下一个焦点控件的查找时曾经通过isRootNamespace()
        方法确定一个 ViewGroup是根控件,并委托它进行控件查找。所以说 setIsRootNamespace()
        是使根控件区别于其他控件的一个重要手段
*/
        mDecor.setIsRootNamespace(true);
        ......
    }
    /*②生成外观模板。根控件 mDecor创建完成之后, mDecor之中没有任何内容,此时它不过是个光杆司令
    接下来的 generateLayout()方法将会完成外观模板的创建,并作为子控件添加中
*/
    if (mContentParent == null){
        /*方法负责生成外观模板,并返回  模板中 使用者控件树的父控件的FrameLayout对象*/
        mContentParent = generateLayout(mDecor);
        /*③从模板中获取具有特定功能的控件并对其进行初始化的属性设置。
        findviewById()委托 mDecor实现,负责查找具有特定id的控件。可见虽然模板的形式可能多种
        多样,不过其中具有相同功能的控件使用了相同的id,以此保证无差别地进行访问*/
        mTitleview=(TextView)findviewById(com.android.internal.R.id.title);
        .......
    }
}
      在此方法中出现了另一个十分重要的成员 mDecor,mDecor是整个控件树的根控件,它是
一个由 generateDecort()方法创建的类型为 DecorView的ViewGroup
DecorView
PhoneWindow的一个内部类,因此它除了担当一个 ViewGroup应有的责任之外,还和
PhoneWindow有着密切的互动。
在接下来的小节中将会介绍 Decor view的特性。
       根控件创建完成之后,便开始通过 generateLayout()方法进行外观模板的创建了。外观模
板的创建是一个非常繁琐的过程,因为它不仅受前文所述窗口特性的影响,而且还需要考虑
窗口的样式设置、 Android的版本等
。 generateLayout()会首先对这些因素进行集中解析。参考
这一部分的代码,因为整个解析过程非常繁杂,这里只挑选几个拥有代表性的解析为例进行
介绍
[PhoneWindow.java-->PhoneWindow.generateLayout()]
protected ViewGroup generateLayout(Decorview decor){
    /*①首先解析窗口样式表。所谓样式表其实是定义在资源系统中的一个xml文件,指定了窗口的各式各样
    的属性。
比如窗口是浮动(对话框)还是全屏( Activity),最小尺寸,是否具有标题栏,是否显示壁纸等。
    这属性设置一部分影响了前文所提到的窗口特性(如标题栏),一部分影了窗口的 LayoutParams
    中的属性(如是否浮动,是否显示壁纸等),还有一部分影响了控件树的工作方式(如最小尺寸,它会
    影响根控件 DecorView的测量)
*/
    // 获取窗口的样式表并存储在变量a中
    TypedArray a = getWindowStyle();
    /*首先以检查样式表中是否定义口为浮动密口(非全屏)为例,这个样式彩响了 Layoutparams中的属性*/
    mIsFloating = a.getBoolean(com.android.internal.R.styleable.window_windowIsFloating,false);
    int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)....
    if (mIsFloating) {
        //对浮动窗口来说,其 Layoutparams.width/ height必须是 WRAP_CONTENT
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        /*并且 LayoutParams.flags中的IN_SCREENINSET_DECOR标记必须被移除。
        因为浮动窗口在布局时不能被状态栏导航栏等遮挡
*/
        setFlags(0, flagsToOpdate);
    } else {
        /*对非浮动窗口来说,其 Layoutparam.width/ height将保持默认的 MATCH_PAREN
          并且需要增加IN_SCREENINSET_DECOR两个标记
*/
        setFlags(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
    }
    //然后检查样式表中是否定义了无标题栏或拥有动作栏两个样式这两个样式影响了窗口的特性
    if (a getBoolean(com. android. internal.R.styleable.window_windowNoTitle, false)){
        /*因为这些样式影啊了窗口的特性,因此 Phonewindow会自行根据样式修改窗口特性。
        这些特性会影响随后外观模板的创建*/
        recuesteeature(FEATURE_NO_TITlE);
    } else if (a.getBoolean(com.android.internal.R
                .styleable.window_windowActionBar, false)){
        requestEeature(FEATURE_ACTION_BAR);
    }
    ...... //其他样式的检查,同样会引发修改LayoutParams以及窗口特性等操作
    /*检查样式中是否定义了最小宽度。这种样式影响了 DecorView测量时的计算。因此其效果并不会体现在
    这里。将样式中的值保存在 mMinwidthMajor/ Minor成员中,并在需要的时候使用。其中 Major
    含义是在横屏情况下的最小宽度,而 Minor则是在竖屏情况下的最小宽度

    在介绍 Decorview时将会体现这些成员的用处*/
    a.getvalue(com.android.internal.R.styleable.window_windowMinwidthMador,mMinwidthMajork);
    a.getvalue(com.android.internal.R.styleable.Window_windowMinWidthMinor,mMinwidthMinorh);
    ......
    //②接下来是影响外观棋板的另外一种因素,即 Android的版本
    final Context context = getContext();
    final int targetsak = context.getApplicationInfo().targetsdkversion;
    final boolean targetPreHoneycomb =
                targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
    .......
    if (targetPreHoneycom
            || (targetpreIcs && targetHCNeedsoptiona && noActionBar){
        /*在 Honeycomb之前的版本中,选项菜单的呼出动作由菜单键完成,因此在需要选项菜单时需要
        导航栏提供虚拟的菜单键。将 NEEDS_MENU_KEY标记放入 LayoutParams中,当此窗口处于熊点
        状态时,WMS会向 SystemUI.请求显示虚拟菜单键
(这部分内容会在第7章讨论)*/
        addFLags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY);
    } else{
        /*面在 Honeycomb之后的版本中,选项菜单由动作栏中的菜单按钮完成,因此要将标记移除*/
        clearFlags(WindowManager, LayoutParams, FLAG NEEDS MEND KEY
    }
    //创建外观模板
    .......

}
      可见 generateLayou()方法在创建外观模板之前会首先解析样式表以及 Android的版本,
再根据这些因素修改 LayoutParams中的设置,申请或删除窗口特性,或者保存一些信息以备
后用。这部分代码体现了使用 Window创建控件树以及管理 LayoutParams的优越性。使用者
仅需声明其所需的样式,而具体的工作都由 Window完成。事实上,窗口所需的样式往往已
经由 Android事先定义好,因此在默认情况下使用者甚至都不用关心样式的存在。
接下来是创建窗口外观的工作
[PhoneWindow.java-->PhoneWindow. generateLayout()]
protected ViewGroup generateLayout (DecorView decor) {
    ......//解祈样式表、 Android版本  的代码
    /*①首先选择合适的外观模板。所有的窗口外观模板巳经实现被定义在系统资源之中。 generatetayout()的工
      作就是根据窗口特性选择一个合适的外观模板的资源id。layoutResource变量保存了选择的结果
*/
    int layoutResource;
    int features = getLocalFeatures();
    if ((features &((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON)))!= 0){
        //如果窗口特性是包含标题栏以及程序图标
        if (mIsFloating) {
            //对浮动窗口来说,其窗口外观被保存在dialogTitleIconsDecorlayout样式中
            Typedvalue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                                    com.android..internal.R.attr.dialogTitleIconsDecorlayout,
                                    res, true);
            layoutResource = res.resourceId;
        } else {
            //对全屏窗口来说,选择 screen_title_icons布局所定义的控件树
            layoutResource = com.android.internal.R.layout.screen_title_icons;
        }
        ......
    }else if ((features &
        ((1 << FEATURE_PRCGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS)))!=0
        && (features & (1 << FEATURE_ACTION_BAR))== 0){
        //如果窗口特性期望有一个进度条,则选择 screen_progreas布局所定义的控件树
        layoutResource = com.android.internal.R.layout.screen_progress;
    }else if(......){//针对其他窗口特性进行选择
        ......
    }
    //②将所选择的布局资源实例化为一棵控件树并保存在变量in之中。这便是最终的外观模板
    View in = mLayoutInFlater.inflate(layoutResource, null);
    //将外观模板作为子控件添加到 Decorview中
    decor.addview(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    /*③从外观模板控件树中获取 作为使用者提供的控件树的父控件的ContemtParent。ContentParent的Id
      是ID_ANDROID_CONTENT。无论哪种模板,必须存在一个拥有此Id的 ViewGroup。
否则 generateLayout()
      将会抛出异常*/
    viewGroup contentparent =(ViewGroup)findviewById(ID_ANDROID_CONTENT);
    ......
    return contentParent;
}
      简单来说, generateLayou()方法花费了较大的精力根据各种窗口特性选择一个系统定
义好的外观模板的布局资源,然后将其实例化后作为子控件添加到根控件 DecorView中。
布局资源的来源有直接定义的(如 screen_title_icon等),还有的是在样式表中定义的(如
dialogTitlelconsDecorLayout等)。
      回过头来看窗口控件树构建的整个过程, PhoneWindow.setContentView()首先调用
installDecor()方法完成外观模板的创建,然后将使用者提供的控件树嵌入模板的mContentParent中。

而 installDecor()方法首先创建一个类型为 DecorView的 ViewGroup作为根
控件,然后使用 generateLayout()方法通过解析样
式、 Android版本、窗口特性等创建合适的模板控
件树。因此在完成 setContentView()的调用之后,
PhoneWindow中便包含一个以 DecorView为根控
件,包含使用者期望显示的内容,外加一系列特
性(标题栏、进度条、动作栏等)作为窗口外观的
一棵控件树,如图6-32所示
与此同时,窗口的
LayoutParams也得到了精心设置。使用者还可以
通过 Window提供的接口修改每一个特性的取值
(标题、进度、图标)。

《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统(完整版)_第28张图片

     为了使窗口得以显示,使用者接下来所需要做的事情就是从 PhoneWindow中获取这一棵
控件树的根控件 DecorView以及 LayoutParams,并通过 WindowManager的接口将其添加到
WMS中。

2. DecorView的特点

      在结束关于 PhoneWindow的讨论之前,有必要讨论一下 DecorView(PhoneWindow独有的ViewGroup)
在使用 Activity或者 Dialog的过程中,一定对它们的成员方法onAttached/DetachedFromWindow()、
dispatch/onTouchEvent()等十分熟悉。 Activity或 Dialog并不是控件系统中的一个控件,
为什么它也会拥有这些控件所特有的回调呢?原来秘密就隐藏在这个 DecorView之中。

      作为 PhoneWindow的一个内部类, DecorView与 PhoneWindow的关系十分紧密。DecorView利用
自己根控件的身份为 PhoneWindow,“偷取”了很多关于控件系统内部的信息
,其中就包括上
述的控件的生命周期信息以及输人事件
      以触摸事件为例,看一下 DecorView.dispatchEvent()的实现:
[PhoneWindow. java-->DecorView.dispatchTouchEvent()]
public boolean dispatchTouchEvent (MotionEvent ev) {
    //callback即 Window.Callback的实例
    final Callback cb= getCallback();
    /*当Callback不为null,并且mFeatureId小于0时(这表示 Decorview是一个顶级窗口的控件树的根
      控件), Decorview直接将触摸事件发送给callback
*/
    return cb != null && !isDestroyed() && mFeatureId <0? cb.dispatchTouchEvent(ev)
        :super.dispatchTouchEvent(ev);
}
可见 DecorView作为整个控件树的根,它并没有像其他 ViewGroup那样将事件派发给子
控件,而是将事件发送给 Window.callback。作为 Window.Callback实现者的 Activity或 Dialog
自然就有能力接收输入事件了,而且它们还能够先于控件树中的其他控件获得处理事件的机
会。
其他的回调诸如onDetachedFromWindow()等也是同理,都是 DecorView这个控件系统的
“叛徒”将这些信息先交给了 Callback所致。

      从代码来看, DecorView将事件交给 Callback处理之后就结束了。那么控件树的其他控件
岂不是没有机会再处理这个事件了?这里以 Activity为例分析一下它的 dispatchTouchEvent()
的实现:
[Activity. java->Activity.dispatchTouchEvent()]
public boolean dispatchTouchEvent(MotionEvent ev)(
    //首先调用Window的superDispatchTouchEvent()
    if (getWindow().superDispatchTouchEvent(ev)){
        //如果 Window的superDispatchTouchEvent()消费了事件,则直接返回
        return true;
    }
    //倘若 Window的superDispatchTouchEvent()没有消费事件,则由Activity.onTouchEvent()处理事件
    return onTouchEvent(ev);
}
那么 Window. superDispatchTouchEvent()做了些什么呢?
[PhoneWindow. java-->PhoneWindow.superDispatchTouchEvent()]
public boolean superDiapatchTouchEvent(MotionEvent event) {
    //事件又转入了 Decorview
    return mDecor.superDispatchTouchEvent(event);

}
再看 DecorView.superDispatchTouchEvent()
[PhoneWindow. java-->DecorView.superDispatchTouchEvent()]
public boolean superDispatchTouchEvent (MotionEvent event) {
    //调用 ViewGroup.dispatchTouchEvent()按照常规流程对事件进行派发
    return super.dispatchTouchEvent();
}
      原来如此,当 DecorView接收到事件之后,会首先将其交给 Callback(即本例中的 Activity)
的 dispatchTouchEvent()过目。 Callback的 dispatchTouchEvent()会将事件交还给 DecorView进行
常规的事件派发,倘若事件在派发过程中没有被消费掉, Callback再自行消费这一事件。这个
过程对于其他事件一样适用。

      因此可以将 DecorView理解为 Callback的实现者在控件树中的替身。 DecorView所接收
的关于控件树的各种事件, Callback的实现者一样可以接收到,就好像它也是控件树中的一员。
可见 Callback的实现者如 Activity或 Dialog中的 dispatchXXXX()会先于控件树中的任何一个
控件进行事件处理,而它们的 onXXXX()则仅当事件没有被任何一个控件消费时才有机会进
行事件处理。

      另外,在分析 PhoneWindow.generateLayou()方法时提到了一个样式用于限制窗口的最小
尺寸。 DecorView负责对这一样式进行实现。它重写了 onMeasured()方法,并在其内部依据样
式的设置限制了测量结果。
在6.3.2节介绍 ViewRootlmpl的预测量时介绍了 ViewRootlmpl对
测量结果的限制。当二者发生冲突时, DecorView的限制拥有更高的决定权

      尽管 DecorView拥有其特殊性,但其特殊性在控件系统中并不会体现出来。对控件系统
来说(包括 View Rootlmpl以及控件树),它与一个普通的控件别无二致。

3.关于 Phonewindow的总结
      关于 PhoneWindow的讨论就此告一段落。相对于之前所讨论的窗口, PhoneWindow
更接近于一个控件系统的概念,因为它的主要工作在于对控件树的操作,而不是用来管理
个窗口。而将它称为一个 Window的原因是其中的两个组件: LayoutParams及控件树正
是通过 WindowManager添加窗口的两个充要条件。因此从 WindowManager的角度来看
PhoneWindow的确是一个不折不扣的窗口。
      经过本节的讨论,创建窗口的方式再次得到了简化,从 WindowManager、 LayoutParam
再加一棵控件树的创建方式变为 WindowManager加上一个 Phonewindow。这次简化的内容
是 LayoutParams的设置以及控件树的构建过程。而 Activity或 Dialog正是以这种方式创建窗
口的

6.6.2 Activity窗口的创建与显示

      本节将从 Activity启动的生命周期中讨论 Activity窗口的创建与显示的过程,并在这个过程
中寻找 PhoneWindow的创建、控件树的创建以及窗口的显示与 Activity的生命周期之间的关系

      当第一次启动 Activity时,第一件事情就是创建一个 Activity的对象(绝大多数情况下这
对象是开发者所实现的 Activity的子类)。而 Activity对象创建完成后的第一件事情就是对
其进行初始化,以便将窗口令牌等重要的信息移交给新生的 Activity。这一初始化的动作发生
在 Activity.attach()方法中。
这一方法的参数非常多,这里省略了与本节内容无关的参数。
[Activity.java->Activity.attach()]
final void attach(...,IBinder token, ...Charsequence title,...){
    ......
    //首先Activity通过PolicyManager创建一个新的 PhoneWindow对象,并保存在 mWindow中
    mWindow = PolicyManager.makeNewWindow(this);
    //然后将Activity作为Window.Callback实例设置给PhonenWindow
    mWindow.setcallback(this);
    // token是IApplicationToken,即添加一个 Activity窗口所需的令牌
    mToken = token;
    ......
    //将一个 WindowManager实例保存到 Phonewindow中,留口令牌也一并做了保存
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED)!=0);
    ......
    //最后从Phonewindow中取出 WindowManager并保存到 Activity中
    mWindowManager = mWindow.getWindowManager();
}
      可见在创建 Activity之后,它立刻便拥有创建窗口所需的所有条件: PhoneWindow、
WindowManager,以及一个来自AMS的窗口令牌。

      接下来就是生成控件树。开发者往往Activity.onCreate(),通过 setContentView()设
置一个布局资源
。如6.6.1节所述, setContentview()将会使得 PhoneWindow完成 Layout
Param的设置以及控件树的创建
。因此在经历 on Created之后, Activity已经随时准备好显示
其窗口
      Activity窗口的显示发生在 onResume()之后,参考 ActivityThread.handleResumeActivity()
方法的相关实现如下
[ActivityThread.java-->ActivityThread.handleResumeActivity()]
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,boolBan reallyResume) {
    .......
    //①在performReeumeActivity()中, Activity.onReaume()方法将会被调用
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r !=null){
        /*②创建窗口
          当 ActivityClientRecord的 window成员为null时,表示此 Activity尚未创建窗口。此时需要
          将 Phonewindow中的控件树交给 WindowManager完成窗口的创建。这种情况对应于 Activity初
          次创建的情况(即 onCreate()被调用的情况)。如果 Activity因为某种原因被暂伴,如新的 Activity
          覆盖其上或者用户按下了HOME键,虽说 Activity不再处于 Resume状态,但是其窗口井没有从WMS中
          移除,只不过它不可见而已
*/
        if (r.window == null &&!a.mFinished && willBeVisible){
            //获取 Phonewindow的实例
            r.window = r.activity.getWindow();
            //获取DecorView

            View decor = r.window.getDecorView();
            //注意 Activity的窗口在初创时是不可见的。因为尚不确定是否真的要显示窗口给用户
            decor.setVisibility(View.INVISIBLE);
            //获取WindowManager
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l =r.window.getAttributes();
            //设置窗口类型为 BASE_APPLICATION。这表示窗口属于一个 Activity
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            if (a.mVisibleFromclient){
                a.mWindowAdded = true;
                //将Decorview添加到WMS 完成窗口的创建
                wm.addView(decor,l);
            }
        }
        //③使Activity可见
        if (!r.activity.mFinished && willBeVisible
            && r.activity.mDecor != null && !r.hideForNow){
            ......
            if (r.activity.mVisibleFromClient){
                //最后通过 Activity.makeVisible()使得 Activity可见
                r.activity.makeVisible();
            }
        }
    }
}
      可见,当 Activity.onResume()被调用时, Activity的窗口其实尚未显示,甚至尚未创建
也就是说 Activity可见发生在 onResume()之后

      其实除非 Activity被销毁(即 onDestroy()被调用),其所属的窗口都会存在于WMS之中
这期间的 onStart()/onStop()所导致的可见性的变化都是通过修改 Decorview的可见性实现窗
口的隐藏与显示的。另外, Activity提供了 Activity.setVisible()方法用于让开发者手动地设置
Activity的可见性,此方法一样是通过更改 DecorView的可见性达到显示或隐藏一个 Activity
的目的。

      至于 Activity窗口的销毁,则发生在 Activity Thread handle DestroyActivityo之中,读者可
以自行研究。
      另外, Dialog一样使用了 Window Manager加 Phone window的方式创建及显示其窗口。在
理解了 Window Manager以及 Phone window的实现原理之后可以很容易地对这一过程进行分
析。本书便不再赞述。

6.7本章小结

      本章着重讨论了控件系统中的几个重要话题,包括 View Rootlmpl的工作原理、控件的测
量布局与绘制、输人事件的派发等
      另外,本章还介绍了另外两种创建窗口的方式:使用 WindowManager、 LayoutParams加
控件树进行手动窗口创建
,以及使用 WindowManager加 Phonewindow通过外观模板进行窗
口创建
再加上第4章介绍的使用WMS加 Surface的创建方式,目前共介绍了三种方式进
行窗口的创建方式。
这三种方式在使用的过程中存在着使用难度上的差异,但是并不是说使
用难度大的创建方式没有用,关键是根据窗口需要完成的任务的不同而选择合适的创建方
式。举例来说,6.6节所介绍的 Activity使用了 PhoneWindow,因为 Activity作为应用程序
主要的显示组件,提供风格统一的UI更加重要。
而运行于 SystemU的状态栏及导航栏需
要与用户频繁进行交互,但是却不需要向应用程序的界面那样形势死板,因此使用灵活的
WindowManager、 Layout Params加控件树的方式更加合适
至于WMS加 Surface这种最底层
的窗口创建方法,也有它存在的意义。 Android壁纸其实是一个窗口,它不需要复杂的用户交
互,却需要提供更加高效的运行效率与更低资源占用,以及更自由的绘图方式,而WMS加
Surface的方式恰恰满足了其需求。

      至此本书通过3章将 WindowManagerService、输人系统以及控件系统这三个负责
Android显示与交互的主要部分讨论完毕。接下来的内容就是讨论建立在这三个系统之上的两
种特点应用: SystemUI以及壁纸了。读者除了要关注两个模块自身的实现原理之外,还应体
会它们是如何完成其窗口的管理以及如何处理用户的交互。

 

 

 

 

 

 

 

---------------------

本文来自 阿拉神农 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/Innost/article/details/47660471?utm_source=copy

你可能感兴趣的:(android书籍阅读笔记)