《深入理解Android 卷III》第七章 深入理解SystemUI(完整版)

                             第7章 深入理解SystemUI

本章主要内容:

·  探讨状态栏与导航栏的启动过程

·  介绍状态栏中的通知信息、系统状态图标等信息的管理与显示原理

·  介绍导航栏中的虚拟按键、SearchPanel的工作原理

·  介绍SystemUIVisibility

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

·  SystemServer.java

frameworks/base/services/java/com/android/server/SystemServer.java

·  SystemUIService.java

frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java

·  PhoneWindowManager.java

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

·  PhoneStatusBar.java

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java

·  BaseStatusBar.java

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java

·  StatusBarManager.java

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

·  StatusBarManagerService.java

frameworks/base/services/java/com/android/server/StatusBarManagerService.java

·  NotificationManager.java

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

·  NotificationManagerService.java

frameworks/base/services/java/com/android/server/NotificationManagerService.java

·  KeyButtonView.java

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java

·  NavigationBarView.java

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java

·  DelegateViewHelper.java

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java

·  SearchPanelView.java

frameworks/base/packages/SystemUI/src/com/android/systemui/SearchPanelView.java

·  PhoneWindow.java

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

·  InputMethodService.java

frameworks/base/core/java/android/inputmethodservice/InputMethodService.java

·  View.java

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

·  ViewRootImpl.java

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

·  WindowManagerService.java

frameworks/base/services/java/com/android/server/wm/WindowManagerService.java

7.1初识SystemUI

顾名思义,SystemUI是为用户提供系统级别的信息显示与交互的一套UI组件,因此它所实现的功能包罗万象。屏幕顶端的状态栏、底部的导航栏图片壁纸以及RecentPanel(近期使用的APP列表)都属于SystemUI的范畴SystemUI中还有一个名为TakeScreenshotService的服务,用于在用户按下音量下键与电源键时进行截屏操作。在第5章曾介绍了PhoneWindowManager监听这一组合键的机制当它捕捉到这一组合键时便会向TakeScreenShotService发送请求从而完成截屏操作SystemUI还提供了PowerUIRingtonePlayer两个服务前者负责监控系统的剩余电量并在必要时为用户显示低电警告,后者则依托AudioService为向其他应用程序提供播放铃声的功能。SystemUI的博大不止如此,读者可以通过查看其AndroidManifest.xml来了解它所实现的其他功能。本章将着重介绍其中最重要的两个功能的实现:状态栏和导航栏。

7.1.1 SystemUIService的启动

尽管SystemUI的表现形式与普通的Android应用程序大相径庭,但它却是以一个APK的形式存在于系统之中,即它与普通的Android应用程序并没有本质上的区别。无非是通过Android四大组件中的Activity、Service、BroadcastReceiver接受外界的请求并执行相关的操作,只不过它们所接受到的请求主要来自各个系统服务而已。

SystemUI包罗万象,并且大部分功能之间相互独立,比如RecentPanel、TakeScreenshotService等均是按需启动,并在完成其既定任务后退出,这与普通的Activity以及Service别无二致。比较特殊的是状态栏、导航栏等组件的启动方式。它们运行于一个称之为SystemUIService的一个Service之中。因此讨论状态栏与导航栏的启动过程其实就是SystemUIService的启动过程

1.SystemUIService的启动时机

那么SystemUIService在何时由谁启动的呢?作为一个系统级别的UI组件,自然要在系统的启动过程中来寻找答案了。

在负责启动各种系统服务的ServerThread中,当核心系统服务启动完成后ServerThread会通过调用ActivityManagerService.systemReady()方法通知AMS系统已经就绪这个systemReady()拥有一个名为goingCallback的Runnable实例作为参数。顾名思义,当AMS完成对systemReady()的处理后将会回调这一Runnable的run()方法。而在这一run()方法中可以找到SystemUI的身影:

[SystemServer.java-->ServerThread]

ActivityManagerService.self().systemReady(new Runnable() {

    public void run() {

        // 调用startSystemUi()

        if(!headless) startSystemUi(contextF);

       ......

    }

}

进一步地,在startSystemUI()方法中:

[SystemServer.java-->ServerThread.startSystemUi()]

static final void startSystemUi(Context context) {

    Intentintent = new Intent();

    // 设置SystemUIService作为启动目标

   intent.setComponent(new ComponentName("com.android.systemui",

               "com.android.systemui.SystemUIService"));

    // 启动SystemUIService

   context.startServiceAsUser(intent, UserHandle.OWNER);

}

可见,当核心的系统服务启动完毕后,ServerThread通过Context.startServiceAsUser()方法完成了SystemUIService的启动。

2.SystemUIService的创建

参考SystemUIService的onCreate()的实现:

[SystemUIService.java-->SystemUIService.onCreate()]

/* ①SERVICES数组定义了运行于SystemUIService之中的子服务列表。当SystemUIService服务启动

  时将会依次启动列表中所存储的子服务 */

final Object[] SERVICES = new Object[] {

        0,// 0号元素存储的其实是一个字符串资源号,这个字符串资源存储了实现了状态栏/导航栏的类名

       com.android.systemui.power.PowerUI.class,

       com.android.systemui.media.RingtonePlayer.class,

    };

 

public void onCreate() {

    ......

   IWindowManager wm = WindowManagerGlobal.getWindowManagerService();

    try {

        /* ② 根据IWindowManager.hasSystemNavBar()的返回值选择一个合适的

          状态栏与导航栏的实现 */

       SERVICES[0] = wm.hasSystemNavBar()

               ? R.string.config_systemBarComponent

               : R.string.config_statusBarComponent;

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

 

    finalint N = SERVICES.length;

    //mServices数组中存储了子服务的实例

   mServices = new SystemUI[N];

    for (inti=0; i

       Class cl = chooseClass(SERVICES[i]);

        try{

           // ③ 实例化子服务并将其存储在mServices数组中

           mServices[i] = (SystemUI)cl.newInstance();

        }catch (IllegalAccessException ex) {......}

        // ④ 设置Context,并通过调用其start()方法运行它

       mServices[i].mContext = this;

       mServices[i].start();

    }

}

除了onCreate()方法之外,SystemUIService没有其他有意义的代码了。显而易见,SystemUIService是一个容器。在其启动时,将会逐个实例化定义在SERVICIES列表中的继承自SystemUI抽象类的子服务。在调用了子服务的start()方法之后,SystemUIService便不再做任何其他的事情,任由各个子服务自行运行。而状态栏导航栏则是这些子服务中的一个。

值得注意的是,onCreate()方法根据IWindowManager.hasSystemNavBar()方法的返回值为状态栏/导航栏选择了不同的实现。进行这一选择的原因为了能够在大尺寸的设备中更有效地利用屏幕空间。在小屏幕设备如手机中,由于屏幕宽度有限,Android采取了状态栏与导航栏分离的布局方案,也就是说导航栏与状态栏占用了更多的垂直空间,使得导航栏的虚拟按键尺寸足够大以及状态栏的信息量足够多。而在大屏幕设备如平板电脑中,由于屏幕宽度比较大,足以在一个屏幕宽度中同时显示足够大的虚拟按键以及足够多的状态栏信息量,此时可以选择将状态栏与导航栏功能集成在一起成为系统栏作为大屏幕下的布局方案,以节省对垂直空间的占用。

hasSystemNavBar()的返回值取决于PhoneWindowManager.mHasSystemNavBar成员的取值。因此在PhoneWindowManager.setInitialDisplaySize()方法中可以得知Android在两种布局方案中进行选择的策略。

[PhoneWindowManager.java-->PhoneWindowManager.setInitialDisplaySize()]

public void setInitialDisplaySize(Display display,int width, intheight, int density) {

    ......

    // ① 计算屏幕短边的DP宽度

    intshortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / density;

 

    // ② 屏幕宽度在720dp以内时,使用分离的布局方案

    if(shortSizeDp < 600) {

        mHasSystemNavBar= false;

       mNavigationBarCanMove = true;

    } elseif (shortSizeDp < 720) {

       mHasSystemNavBar = false;

       mNavigationBarCanMove = false;

    }

    ......

}

在SystemUI中,分离布局方案的实现者是PhoneStatusBar而集成布局方案的实现者则是TabletStatusBar。二者的本质功能是一致的,即提供虚拟按键、显示通知信息等区别仅在于布局的不同、以及由此所衍生出的定制行为而已。因此不难想到,它们是从同一个父类中继承出来的。这一父类的名字是BaseStatusBar。本章将主要介绍PhoneStatusBar的实现,读者可以类比地对TabletStatusBar进行研究。

7.1.2 状态栏与导航栏的创建

如7.1.1节所述,状态栏与导航栏的启动由其PhoneStatusBar.start()完成。参考其实现:

[PhoneStatusBar.java-->PhoneStatusBar.start()]

public void start() {

    ......

    // ① 调用父类BaseStatusBar的start()方法进行初始化。

   super.start();

    // 创建导航栏的窗口

    addNavigationBar();

    // ② 创建PhoneStatusBarPolicy。PhoneStatusBarPolicy定义了系统通知图标的设置策略

   mIconPolicy = new PhoneStatusBarPolicy(mContext);

}

参考BaseStatusBar.start()的实现,这段代码比较长,并且涉及到了本章随后会详细介绍的内容。因此倘若读者阅读起来比较吃力可以仅关注那三个关键步骤。在完成本章的学习之后再回过头来阅读这部分代码便会发现十分简单了。

[BaseStatusBar-->BaseStatusBar.start()]

public void start() {

    /* 由于状态栏的窗口不属于任何一个Activity,所以需要使用第6章所介绍的WindowManager

      进行窗口的创建 */

   mWindowManager = (WindowManager)mContext

                               .getSystemService(Context.WINDOW_SERVICE);

    /* 在第4章介绍窗口的布局时曾经提到状态栏的存在对窗口布局有着重要的影响。因此状态栏中

      所发生的变化有必要通知给WMS */

   mWindowManagerService = WindowManagerGlobal.getWindowManagerService();

    ......

 

    /*mProvisioningOberver是一个ContentObserver

      它负责监听Settings.Global.DEVICE_PROVISIONED设置的变化。这一设置表示此设备是否已经

      归属于某一个用户。比如当用户打开一个新购买的设备时,初始化设置向导将会引导用户阅读使用条款、

      设置帐户等一系列的初始化操作。在初始化设置向导完成之前,

      Settings.Global.DEVICE_PROVISIONED的值为false,表示这台设备并未归属于某

      一个用户。

      当设备并未归属于某以用户时,状态栏会禁用一些功能以避免信息的泄露。mProvisioningObserver

      即是用来监听设备归属状态的变化,以禁用或启用某些功能 */

   mProvisioningObserver.onChange(false); // set up

   mContext.getContentResolver().registerContentObserver(

           Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true,

           mProvisioningObserver);

 

    /* ① 获取IStatusBarService的实例。IStatusBarService是一个系统服务,由ServerThread

      启动并常驻system_server进程中。IStatusBarService为那些对状态栏感兴趣的其他系统服务定

      义了一系列API,然而对SystemUI而言,IStatusBarService更像是一个客户端。因为IStatusBarService会将操作

      状态栏的请求发送给SystemUI,并由后者完成请求 */

   mBarService = IStatusBarService.Stub.asInterface(

           ServiceManager.getService(Context.STATUS_BAR_SERVICE));

 

    /* 随后BaseStatusBar(状态栏、导航栏)将自己注册到IStatusBarService之中。以此声明本实例才是状态栏的真正

      实现者,IStatusBarService会将其所接受到的请求转发给本实例。

      “天有不测风云”,SystemUI难免会因为某些原因使得其意外终止。而状态栏中所显示的信息并不属于状态

      栏自己,而是属于其他的应用程序或是其他的系统服务。因此当SystemUI重新启动时,便需要恢复其

      终止前所显示的信息以避免信息的丢失。为此,IStatusBarService中保存了所有的需要状态栏进行显

      示的信息的副本,并在新的状态栏实例启动后,这些副本将会伴随着注册的过程传递给状态栏并进行显示,

      从而避免了信息的丢失。

      从代码分析的角度来看,这一从IStatusBarService中取回信息副本的过程正好完整地体现了状态栏

      所能显示的信息的类型*/

 

    /*iconList是向IStatusBarService进行注册的参数之一。它保存了用于显示在状态栏的系统状态

      区中的状态图标列表在完成注册之后,IStatusBarService将会在其中填充两个数组,一个字符串

      数组用于表示状态的名称,一个StatusBarIcon类型的数组用于存储需要显示的图标资源。

      关于系统状态区的工作原理将在7.2.3节介绍*/

   StatusBarIconList iconList = new StatusBarIconList();

    /*notificationKeysStatusBarNotification则存储了需要显示在状态栏的通知区中通知信息。

      前者存储了一个用Binder表示的通知发送者的ID列表。而notifications则存储了通知列表。二者

      通过索引号一一对应。关于通知的工作原理将在7.2.2节介绍 */

   ArrayList notificationKeys = newArrayList();

   ArrayList notifications  = newArrayList();

    /*mCommandQueue是CommandQueue类的一个实例。CommandQueue继承自IStatusBar.Stub。

      因此它是IStatusBar的Bn端在完成注册后,这一Binder对象的Bp端将会保存在

     IStatusBarService之中。因此它是IStatusBarService与BaseStatusBar进行通信的桥梁。

      */

    mCommandQueue= new CommandQueue(this, iconList);

    /*switches则存储了一些杂项:禁用功能列表,SystemUIVisiblity,是否在导航栏中显示虚拟的

      菜单键,输入法窗口是否可见、输入法窗口是否消费BACK键、是否接入了实体键盘、实体键盘是否被启用。

      在后文中将会介绍它们的具体影响 */

    int[] switches = new int[7];

   ArrayList binders = new ArrayList();

    try {

        // ② 向IStatusBarServie进行注册,并获取所有保存在IStatusBarService中的信息副本

       mBarService.registerStatusBar(mCommandQueue, iconList,

                                       notificationKeys,notifications,

                                      switches, binders);

    } catch(RemoteException ex) {......}

 

    // ③ 创建状态栏与导航栏的窗口。由于创建状态栏与导航栏的窗口涉及到控件树的创建,因此它由子类

    PhoneStatusBar或TabletStatusBar实现,以根据不同的布局方案选择创建不同的窗口与控件树 */

   createAndAddWindows();

 

    /*使用来自IStatusBarService中所获取的信息

      mCommandQueue已经注册到IStatusBarService中,状态栏与导航栏的窗口与控件树也都创建完毕

      因此接下来的任务就是应用从IStatusBarService中所获取的信息 */

   disable(switches[0]); // 禁用某些功能

   setSystemUiVisibility(switches[1], 0xffffffff); // 设置SystemUIVisibility

    topAppWindowChanged(switches[2]!= 0); // 设置菜单键的可见性

    // 根据输入法窗口的可见性调整导航栏的样式

   setImeWindowStatus(binders.get(0), switches[3], switches[4]);

    // 设置硬件键盘信息。

   setHardKeyboardStatus(switches[5] != 0, switches[6] != 0);

 

    // 依次向系统状态区添加状态图标

    int N = iconList.size();

    ......

    // 依次向通知栏添加通知

    N = notificationKeys.size();

    ......

 

    /* 至此,与IStatusBarService的连接已建立,状态栏与导航栏的窗口也已完成创建与显示,并且

      保存在IStatusBarService中的信息都已完成了显示或设置。状态栏与导航栏的启动正式完成 */

}

可见,状态栏与导航栏的启动分为如下几个过程:

·  获取IStatusBarService,IStatusBarService是运行于system_server的一个系统服务,它接受操作状态栏/导航栏的请求并将其转发给BaseStatusBar。为了保证SystemUI意外退出后不会发生信息丢失,IStatusBarService保存了所有需要状态栏与导航栏进行显示或处理的信息副本。

·  将一个继承自IStatusBar.Stub的CommandQueue的实例注册到IStatusBarService以建立通信,并将信息副本取回。

·  通过调用子类的createAndAddWindows()方法完成状态栏与导航栏的控件树及窗口的创建与显示。

·  使用从IStatusBarService取回的信息副本。

7.1.3 理解IStatusBarService

那么IStatusBarService的真身如何呢?它的实现者是StatusBarManagerService。由于状态栏导航栏与它的关系十分密切,因此需要对其有所了解。

与WindowManagerService、InputManagerService等系统服务一样,StatusBarManagerService在ServerThread中创建。参考如下代码:

[SystemServer.java-->ServerThread.run()]

public void run() {

    try {

        /* 创建一个StatusBarManagerService的实例,并注册到ServiceManager中使其成为

          一个系统服务 */

       statusBar = new StatusBarManagerService(context, wm);

       ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);

    } catch(Throwable e) {......}

}

再看其构造函数:

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

public StatusBarManagerService(Context context,WindowManagerService windowManager) {

    mContext= context;

   mWindowManager = windowManager;

    // 监听实体键盘的状态变化

   mWindowManager.setOnHardKeyboardStatusChangeListener(this);

    // 初始化状态栏的系统状态区的状态图标列表。

    final Resources res = context.getResources();

    mIcons.defineSlots(res.getStringArray(com.android.internal.R.array.config_statusBarIcons));

}

这基本上是系统服务中最简单的构造函数了,在这里并没有发现能够揭示StatusBarManagerService的工作原理的线索(由此也可以预见StatusBarManagerService的实现十分简单)。

接下来参考StatusBarManagerService.registerStatusBar()的实现。这个方法由SystemUI中的BaseStatusBar调用,用于建立其与StatusBarManagerService的通信连接,并取回保存在其中的信息副本。

[StatusBarManagerService.java-->StatusBarManagerService.registerStatusBar()]

public void registerStatusBar(IStatusBar bar,StatusBarIconList iconList,

        List notificationKeys,List notifications,

        intswitches[], List binders) {

    /* 首先是权限检查。状态栏与导航栏是Android系统中一个十分重要的组件,因此必须避免其他应用

      调用此方法对状态栏与导航栏进行偷梁换柱。因此要求方法的调用者必须具有一个签名级的权限

       android.permission.STATUS_BAR_SERVICE*/

   enforceStatusBarService();

    /* ① 将bar参数保存到mBar成员中。bar的类型是IStatusBar,它即是BaseStatusBar中的

     CommandQueue的Bp端。从此之后,StatusBarManagerService将通过mBar与BaseStatusBar

      进行通信。因此可以理解mBar就是SystemUI中的状态栏与导航栏 */

    mBar =bar;

 

    // ② 接下来依次为调用者返回信息副本

    // 系统状态区的图标列表

   synchronized (mIcons) { iconList.copyFrom(mIcons); }

    // 通知区的通知信息

   synchronized (mNotifications) {

        for(Map.Entry e: mNotifications.entrySet()) {

           notificationKeys.add(e.getKey());

           notifications.add(e.getValue());

        }

    }

    //switches中的杂项

   synchronized (mLock) {

       switches[0] = gatherDisableActionsLocked(mCurrentUserId);

        ......

    }

    ......

}

可见StatusBarManagerService.registerStatusBar()的实现也十分简单。主要是保存BaseStatusBar中的CommandQueue的Bp端到mBar成员之中,然后再把信息副本填充到参数里去。尽管简单,但是从其实现中可以预料到StatusBarManagerService的工作方式:当它接受到操作状态栏与导航栏的请求时,首先将请求信息保存到副本之中,然后再将这一请求通过mBar发送给BaseStatusBar。以设置系统状态区图标这一操作为例,参考如下代码:

[StatusBarManagerService.java-->StatusBarManagerService.setIcon()]  // 设置系统状态区图标

public void setIcon(String slot, StringiconPackage, int iconId, int iconLevel,

       String contentDescription) {

    /* 首先一样是权限检查,与registerStatusBar()不同,这次要求的是一个系统级别的权限

      android.permission.STATUS_BAR。因为设置系统状态区图标的操作不允许普通应用程序进行。

      其他的操作诸如添加一条通知则不需要此权限 */

   enforceStatusBar();

 

   synchronized (mIcons) {

        intindex = mIcons.getSlotIndex(slot);

        ......

       StatusBarIcon icon = new StatusBarIcon(iconPackage, UserHandle.OWNER,iconId,

               iconLevel, 0,

               contentDescription);

        // ① 将图标信息保存在副本之中

       mIcons.setIcon(index, icon);

        // ② 将设置请求发送给BaseStatusBar

        if(mBar != null) {

           try {

               mBar.setIcon(index, icon);

           } catch (RemoteException ex) {......}

        }

    }

}

纵观StatusBarManagerService中的其他方法,会发现它们与setIcon()方法的实现十分类似。从而可以得知StatusBarManagerService的作用与工作原理如下:

·  它是SystemUI中的状态栏与导航栏在system_server中的代理。所有对状态栏或导航来有需求的对象都可以通过获取StatusBarManagerService的实例或Bp端达到其目的。只不过使用者必须拥有能够完成操作的相应权限。

·  它保存了状态栏/导航栏所需的信息副本,用于在SystemUI意外退出之后的恢复。

7.1.4 SystemUI的体系结构

完成了对SystemUI的启动过程的分析之后便可以对其体系结构做出总结,如图7-1所示。

·  SystemUIService,一个普通的Android服务,它以一个容器的角色运行于SystemUI进程中。在它内部运行着多个子服务,其中之一便是状态栏与导航栏的实现者——BaseStatusBar的子类之一。

·  IStatusBarService,即系统服务StatusBarManagerService是状态栏导航栏向外界提供服务的前端接口,运行于system_server进程中。

·  BaseStatusBar及其子类是状态栏与导航栏的实际实现者,运行于SystemUIService中。

·  IStatusBar,即SystemUI中的CommandQueue是联系StatusBarManagerService与BaseStatusBar的桥梁。

·  SystemUI中还包含了ImageWallpaperRecentPanel以及TakeScreenshotService等功能的实现。它们是Service、Activity等标准的Android应用程序组件,并且互相独立。对这些功能感兴趣的使用者可以通过startService()/startActivity()等方式方便地启动相应的功能。

 

图 7 - 1 SystemUI的体系结构

在本章将主要介绍SystemUI中最常用的状态栏、导航栏以及RecentPanel的实现。ImageWallpaper将在第8章中进行详细地介绍。而SystemUI其他的功能读者可以自行研究。

7.2 深入理解状态栏

如7.1.1节所述,SystemUI中存在两种状态栏与导航栏的实现——即状态栏与导航栏分离的布局的PhoneStatusBar以及状态栏与导航栏集成布局的TabletStatusBar两种。除了布局差异之外,二者并无本质上的差别,因此本节将主要介绍PhoneStatusBar下的状态栏的实现。

作为一个将所有信息集中显示的场所,状态栏对需要显示的信息做了以下的五个分类

·  通知信息它可以在状态栏左侧显示一个图标以引起用户的主意,并在下拉卷帘中为用户显示更加详细的信息。这是状态栏所能提供的信息显示服务之中最灵活的一种功能。它对信息种类以及来源没有做任何限制。使用者可以通过StatusBarManagerService所提供的接口向状态栏中添加或移除一条通知信息。

·  时间信息显示在状态栏最右侧的一个小型数字时钟,是一个名为Clock的继承自TextView的控件。它监听了几个和时间相关的广播:ACTION_TIME_TICKACTION_TIME_CHANGEDACTION_TIMEZONE_CHANGED以及ACTION_CONFIGURATION_CHANGED。当其中一个广播到来时从Calendar类中获取当前的系统时间,然后进行字符串格式化后显示出来。时间信息的维护工作在状态栏内部完成,因此外界无法通过API修改时间信息的显示或行为。

·  电量信息:显示在数字时钟左侧的一个电池图标,用于提示设备当前的电量情况。它是一个被BatteryController类所管理的ImageViewBatteryController通过监听android.intent.action.BATTERY_CHANGED广播以从BetteryService中获取电量信息,并根据电量信息选择一个合适的电池图标显示在ImageView上。同时间信息一样,这也是在状态栏内部维护的,外界无法干预状态栏对电量信息的显示行为。

·  信号信息:显示在电量信息的左侧的一系列ImageView用于显示系统当前的Wifi、移动网络的信号状态。用户所看到的Wifi图标、手机信号图标、飞行模式图标都属于信号信息的范畴它们被NetworkController类维护着。NetworkController监听了一系列与信号相关的广播如WIFI_STATE_CHANGED_ACTION、ACTION_SIM_STATE_CHANGED、ACTION_AIRPLANE_MODE_CHANGED等,并在这些广播到来时显示、更改或移除相关的ImageView。同样,外界无法干预状态栏对信号信息的显示行为。

·  系统状态图标区:这个区域用一系列图标标识系统当前的状态,位于信号信息的左侧,与状态栏左侧通知信息隔岸相望。通知信息类似,StatusBarManagerService通过setIcon()接口为外界提供了修改系统状态图标区的图标的途径,然而它对信息的内容有很强的限制首先,系统状态图标区无法显示图标以外的信息,另外,系统状态图标区的对其所显示的图标数量以及图标所表示的意图有着严格的限制。

由于时间信息、电量信息以及信号信息的实现原理比较简单而且与状态栏外界相对隔离,因此读者可以通过分析上文所介绍的相关组件自行研究。本节将主要介绍状态栏的一下几个方面的内容:

·  状态栏窗口的创建与控件树结构。

·  通知的管理与显示。

·  系统状态图标区的管理与显示。

7.2.1 状态栏窗口的创建与控件树结构

1. 状态栏窗口的创建

在7.1.2节所引用的BaseStatusBar.start()方法的代码中调用了createAndAddWindows()方法进行状态栏窗口的创建。很显然,createAndAddWindow()由PhoneStatusBar或TabletStatusBar实现。以PhoneStatusBar为例,参考其代码:

[PhoneStatusBar.java-->PhoneStatusBar.createAndAddWindow()]

public void createAndAddWindows() {

   addStatusBarWindow(); // 直接调用addStatusBarWindow()方法

}

在addStatusBarWindow()方法中,PhoneStatusBar将会构建状态栏的控件树并通过WindowManager的接口为其创建窗口。

[PhoneStatusBar.java-->PhoneStatusBar.addStatusBarWindow()]

private void addStatusBarWindow() {

    // ① 通过getStatusBarHeight()方法获取状态栏的高度

    finalint height = getStatusBarHeight();

 

    // ② 为状态栏创建WindowManager.LayoutParams

    finalWindowManager.LayoutParams lp = new WindowManager.LayoutParams(

           ViewGroup.LayoutParams.MATCH_PARENT, // 状态栏的宽度为充满整个屏幕宽度

           height, // 高度来自于getStatusBarHeight()方法

           WindowManager.LayoutParams.TYPE_STATUS_BAR, // 窗口类型

           WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE // 状态栏不接受按键事件

                 /* FLAG_TOUCHABLE_WHEN_WAKING这一标记将使得状态栏接受导致设备唤醒的触摸

                   事件。通常这一事件会在interceptMotionBeforeQueueing()的过程中被用于

                   唤醒设备(或从变暗状态下恢复),而InputDispatcher会阻止这一事件发送给

                   窗口。*/

               | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING

                  // FLAG_SPLIT_TOUCH允许状态栏支持触摸事件序列的拆分

               | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,

           PixelFormat.TRANSLUCENT); // 状态栏的Surface像素格式为支持透明度

    // 启用硬件加速

    lp.flags|= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;

    //StatusBar的gravity是LEFT和FILL_HORIZONTAL

   lp.gravity = getStatusBarGravity();

   lp.setTitle("StatusBar");

   lp.packageName = mContext.getPackageName();

 

    // ③ 创建状态栏的控件树

   makeStatusBarView();

 

    // ④ 通过WindowManager.addView()创建状态栏的窗口

   //状态栏控件树的根控件被保存在mStatusBarWindow成员中

   mWindowManager.addView(mStatusBarWindow, lp);

}

此方法提供了很多重要的信息。

首先是状态栏的高度,由getStatusBarHeight()从资源com.android.internal.R.dimen.status_bar_height中获得。这一资源定义在frameworks\base\core\res\res\values\dimens.xml中,默认为25dip。此资源同样在PhoneWindowManager中被用来计算作为布局准绳的八个矩形。

然后是状态栏窗口的LayoutParams的创建。LayoutParams描述了状态栏是怎样的一个窗口。TYPE_STATUS_BAR使得PhoneWindowManager为状态栏的窗口分配了较大的layer值,使其可以显示在其他应用窗口之上。FLAG_NOT_FOCUSABLE、FLAG_TOUCHABLE_WHEN_WAKING和FLAG_SPLIT_TOUCH则定义了状态栏对输入事件的响应行为。

注意 通过创建窗口所使用的LayoutParams来推断一个窗口的行为十分重要。在分析一个需要创建窗口的模块的工作原理时,从窗口创建过程往往是一个不错的切入点。

另外需要知道的是,窗口创建之后,其LayoutParams是会发生变化的。以状态栏为例,创建窗口时其高度为25dip,flags描述其不可接收按键事件。不过当用户按下状态栏导致卷帘下拉时,PhoneStatusBar会通过WindowManager.updateViewLayout()方法修改窗口的LayoutParams的高度为MATCH_PARENT,即充满整个屏幕以使得卷帘可以满屏显示,并且移除FLAG_NOT_FOCUSABLE,使得PhoneStatusBar可以通过监听BACK键以收回卷帘。

在makeStatusBarView()完成控件树的创建之后,WindowManager.addView()将根据控件树创建出状态栏的窗口。显而易见,状态栏控件树的根控件被保存在mStatusBarWindow成员中。

createStatusBarView()负责从R.layout.super_status_bar所描述的布局中实例化出一棵控件树。并从这个控件树中取出一些比较重要的控件并保存在对应的成员变量中。因此从R.layout.super_status_bar入手可以很容易地得知状态栏的控件树的结构:

2.状态栏控件树的结构

参考SystemUI下 super_status_bar.xml所描述的布局内容,可以看到其根控件是一个名为StatusBarWindowView的控件,它继承自FrameLayout。在其下的两个直接子控件如下:

·  @layout/status_bar所描述的布局。这是用户平时所见的状态栏。

·  PenelHolder:这个继承自FrameLayout的控件是状态栏的卷帘。在其下的两个直接子控件@layout/status_bar_expanded以及@layout/quick_settings分别对应于卷帘之中的通知列表面板以及快速设定面板

在正常情况下,StatusBarWindowView中只有@layout/status_bar所描述的布局是可见的,并且状态栏窗口为com.android.internal.R.dimen.status_bar_height所定义的高度。当StatusBarWindowView截获了ACTION_DOWN的触摸事件后,会修改窗口的高度为MATCH_PARENT,然后将PenelHolder设为可见并跟随用户的触摸轨迹,由此实现了卷帘的下拉效果。

说明 PenelHolder继承自FrameLayout。那么它如何做到在@layout/status_bar_expanded以及@layout/quick_settings两个控件之间进行切换显示呢?答案就在第6章所介绍的ViewGroup. getChildDrawingOrder()方法中。此方法的返回值影响了子控件的绘制顺序,同时也影响了控件接收触摸事件的优先级。当PenelHolder希望显示@layout/status_bar_expanded面版时,它在此方法中将此面版的绘制顺序放在最后,使其在绘制时能够覆盖@layout/quick_settings,并且优先接受触摸事件。反之则将@layout/quick_settings的绘制顺序放在最后即可。

因此状态栏控件树的第一层结构如图7-2所示。

 

图 7 - 2状态栏控件树的结构1

再看status_bar.xml所描述的布局内容,其根控件是一个继承自FrameLayout的名为StatusBarView类型的控件,makeStatusBarView()方法会将其保存为mStatusBarView。其直接子控件有三个:

·  @id/notification_lights_out,一个ImageView,并且一般情况下它是不可见的在SystemUIVisiblity中有一个名为SYSTEM_UI_FLAG_LOW_PROFILE的标记。当一个应用程序希望让用户的注意力更多地集中在它所显示的内容时,可以在其SystemUIVisibility中添加这一标记。SYSTEM_UI_FLAG_LOW_PROFILE会使得状态栏与导航栏进入低辨识度模式。低辨识度模式下的状态栏将不会显示任何信息,只是在黑色背景中显示一个灰色圆点而已。而这一个黑色圆点即是这里的id/notification_lights_out。

·  @id/status_bar_contents一个LinearLayout,状态栏上各种信息的显示场所。

·  @id/ticker一个LinearLayout,其中包含了一个ImageSwitcher和一个TickerView。在正常情况下@id/ticker是不可见的。当一个新的通知到来时(例如一条新的短信),状态栏上会以动画方式逐行显示通知的内容,使得用户可以在无需下拉卷帘的情况下了解新通知的内容。这一功能在状态栏中被称之为Ticker。而@id/ticker则是完成Ticker功能的场所。makeStatusBarView()会将@id/ticker保存为mTickerView。

至此,状态栏控件树的结构可以扩充为图7-3所示。

 

                                                                                  图 7 - 3状态栏控件树的结构2

再来分析@id/status_bar_contents所包含的内容。如前文所述,状态栏所显示的信息共有5种,因此@id/status_bar_contents中的子控件分别用来显示这5种信息。其中通知信息显示在@id/notification_icon_area里,而其他四种信息则显示在@id/system_icon_area之中。

·  @id/notification_icon_area一个LinearLayout。包含了两个子控件分别是类型为StatusBarIconView@id/moreIcon以及一个类型为IconMerger@id/notificationIconsIconMerger继承自LinearLayout通知信息的图标都会以一个StatusBarIconView的形式存储在IconMerger之中而IconMeger和LinearLayout的区别在于,如果它在onLayout()的过程中发现会其内部所容纳的StatusBarIconView的总宽度超过了它自身的宽度,则会设置@id/moreIcon为可见,使得用户得知有部分通知图标因为显示空间不够而被隐藏makeStausBarView()会将@id/notificationIcons保存为成员变量mNotificationIcons。因此当新的通知到来时,只要将一个StatusBarIconView放置到mNotificationIcons即可显示此通知的图标了。

·  @id/system_icon_area也是一个LinearLayout。它容纳了除通知信息的图标以外的四种信息的显示。在其中有负责显示时间信息的@id/clock,负责显示电量信息的@id/battery,负责信号信息显示的@id/signal_cluster以及负责容纳系统状态区图标的一个LinearLayout——@id/statusIcons。其中@id/statusIcons会被保存到成员变量mStatusIcons中,当需要显示某一个系统状态图标时,将图标放置到mStatusIcons中即可。

注意 @id/system_icon_area的宽度定义为WRAP_CONTENT,而@id/notification_icon_area的weight被设置为1。在这种情况下,@id/system_icon_area将在状态栏右侧根据其所显示的图标个数调整其尺寸。而@id/notification_icon_area则会占用状态栏左侧的剩余空间。这说明了一个问题:系统图标区将优先占用状态栏的空间进行信息的显示。这也是IconMerger类以及@id/moreIcon存在的原因。

于是可以将图7-3扩展为图7-4。

 

                                                                                  图 7 - 4状态栏控件树的结构3

另外,在@layout/status_bar_expanded之中有一个类型为NotificationRowLayout的控件@id/latestItems,并且会被makeStatusBarView()保存到mPile成员变量中。它位于下拉卷帘中,是通知信息列表的容器。

在分析控件树结构的过程中发现了如下几个重要的控件:

·  mStatusBarWindow,整个状态栏的根控件。它包含了两棵子控件树,分别是常态下的状态栏以及下拉卷帘。

·  mStatusBarView常态下的状态栏。它所包含的三棵子控件树分别对应了状态栏的三种工作状态——低辨识度模式、Ticker以及常态。这三棵控件树会随着这三种工作状态的切换交替显示。

·  mNotificationIcons,继承自LinearLayout的IconMerger控件的实例,负责容纳通知图标。当mNotificationIcons的宽度不足以容纳所有通知图标时,会将@id/moreIcon设置为可见以告知用户存在未显示的通知图标。

·  mTickerView,实现了当新通知到来时的动画效果,使得用户可以在无需下拉卷帘的情况下了解新通知的内容。

·  mStatusIcons,一个LinearLayout,它是系统状态图标区,负责容纳系统状态图标。

·  mPile,一个NotificationRowLayout,它作为通知列表的容器被保存在下拉卷帘中。因此当一个通知信息除了需要将其图标添加到mNotificationIcons以外,还需要将其详细信息(标题、描述等)添加到mPile中,使得用户在下来卷帘中可以看到它。

对状态栏控件树的结构分析至此便告一段落了。接下来将从通知信息以及系统状态图标两个方面介绍状态栏的工作原理。希望读者能够理解本节所介绍的几个重要控件所在的位置以及其基本功能,这将使得后续内容的学习更加轻松。

7.2.2 通知信息的管理与显示

通知信息是状态栏中最常用的功能之一。根据用户是否拉下下拉卷帘,通知信息表现为一个位于状态栏的图标,或在下拉卷帘中的一个条目。另外,通知信息还可以在其添加入状态栏之时发出声音,以提醒用户注意查看。通知信息即可以表示一条事件,如新的短消息到来、出现了一条未接来电等,也可以用来表示一个正在后台持续进行着的工作,如正在下载某一文件、正在播放音乐等。

1.通知信息的发送

任何使用者都可以通过NotificationManager所提供的接口向状态栏添加一则通知信息。通知信息的详细内容可以通过一个Notification类的实例来描述。

Notification类中包含如下几个用于描述通知信息的关键字段。

·  icon,一个用于描述一个图标的资源id,用于显示在状态栏之上。每条通知信息必须提供一个有效的图标资源,否则此信息将会被忽略。

·  iconLevel如果icon所描述的图标资源存在level,那么iconLevel则用于告知状态栏将显示图标资源的那一个level。

·  number,一个int型变量用于表示通知数目。例如,当有3条新的短信时,没有必要使用三个通知,而是将一个通知的number成员设置为3,状态栏会将这一数字显示在通知图标上。

·  contentIntent,一个PendingIntent的实例,用于告知状态栏当在下拉卷帘中点击本条通知时应当执行的动作。contentIntent往往用于启动一个Activity以便让用户能够查看关于此条通知的详细信息。例如,当用户点击一条提示新短信的通知时,短信应用将会被启动并显示短信的详细内容。

·  deleteIntent,一个PendingIntent的实例,用于告知状态栏当用户从下拉卷帘中删除本条通知时应当执行的动作。deleteIntent往往用在表示某个工作正在后台进行的通知中,以便当用户从下拉卷帘中删除通知时,发送者可以终止此后台工作。

·  tickerText一条文本。当通知信息被添加时,状态栏将会在其上逐行显示这条信息。其目的在于使用户无需进行卷帘下拉操作即可从快速获取通知的内容。

·  fullScreenIntent,一个PendingIntent的实例,用于告知状态栏当此条信息被添加时应当执行的动作一般这一动作是启动一个Activity用于显示与通知相关的详细信息。fullScreenIntent其实是一个替代tickerText的设置。当Notification中指定了fullScreenIntent时,StatusBar将会忽略tickerText的设置。因为这两个设置的目的都是为了让用户可以在第一时间了解通知的内容。不过相对于tickerText,fullScreenIntent强制性要明显得多,因为它将打断用户当前正在进行的工作。因此fullScreenIntent应该仅用于通知非常重要或紧急的事件,比如说来电或闹钟。

·  contentView/bigContentView,RemoteView的实例,可以用来定制通知信息在下拉卷帘中的显示形式。一般来讲,相对于contentView,bigContentView可以占用更多空间以显示更加详细的内容。状态栏将根据自己的判断选择将通知信息显示为contentView或是bigContentView。

·  sound与audioStreamType指定一个用于播放通知声音的Uri及其所使用的音频流类型。在默认情况下,播放通知声音所用的音频流类型为STREAM_NOTIFICATION。

·  vibrate,一个float数组,用于描述震动方式。

·  ledARGB/ledOnMS/ledOffMS,指定当此通知被添加到状态栏时设备上的LED指示灯的行为,这几个设置需要硬件设备的支持。

·  defaults,用于指示声音、震动以及LED指示灯是否使用系统的默认行为。

·  flags,用于存储一系列用于定制通知信息行为的标记。通知信息的发送者可以根据需求在其中加入这样的标记:FLAG_SHOW_LIGHTS要求使用LED指示灯,FLAG_ONGOING_EVENT指示通知信息用于描述一个正在进行的后台工作,FLAG_INSISTENT指示通知声音将持续播放直到通知信息被移除或被用户查看,FLAG_ONLY_ARLERT_ONCE指示任何时候通知信息被加入到状态栏时都会播放一次通知声音,FLAG_AUTO_CANCEL指示当用户在下拉卷帘中点击通知信息时自动将其移出,FLAG_FOREGROUND_SERVICE指示此通知用来表示一个正在以foreground形式运行的服务。

·  priority,描述了通知的重要性级别。通知信息的级别从低到高共分为MIN(-2)、LOW(-1)、DEFAULT(0)以及HIGH(1)四级。低优先级的通知信息有可能不会被显示给用户,或显示在通知列表中靠下的位置。

在随后的讨论中将会详细介绍这些信息如何影响通知信息的显示与行为。

当通知信息的发送者根据需求完成了Notification实例的创建之后,便可以通过NotificationManager.notify()方法将通知显示在状态栏上。

notify()方法要求通知信息的发送者除了提供一个Notification实例之外,还需要提供一个字符串类型的参数tag,以及int类型的参数id,这两个参数一并确定了信息的意图。当一条通知信息已经被提交给NotificationManager.notify()并且仍然显示在状态栏中时,它将会被新提交的拥有相同意图(即相同的tag以及相同的id)通知信息所替换。

参考NotificationManager.notify()方法的实现:

[NotificationManager.java-->NotificationManager.notify()]

public void notify(String tag, int id,Notification notification)

{

    int[]idOut = new int[1];

    // ① 获取NotificationManagerService的Bp端代理

   INotificationManager service = getService();

    // ② 获取信息发送者的包名

    Stringpkg = mContext.getPackageName();

    ......

    try {

        // ③ 将包名、tag、id以及Notification实例一并提交给NotificationManagerService

       service.enqueueNotificationWithTag(pkg, tag, id, notification, idOut, UserHandle.myUserId());

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

}

NotificationManager会将通知信息发送给NotificationManagerService,并由NotificationManagerService对信息进行进一步处理。注意Notification将通知发送者的包名作为参数传递给了NotificationManagerService。对于一个应用程序来说,tag与id而这一起确定了通知的意图。由于NotificationManagerService作为一个系统服务需要接受来自各个应用程序通知信息,因此对NotificationManagerService来说,确定通知的意图需要在tag与id之外再增加一项:通知发送者的包名。因此由于包名的不一样,来自两个应用程序的具有相同tag与id的通知信息之间不会发生任何冲突。另外将包名作为通知意图的元素之一的原因出于对信息安全考虑。

而将一则通知信息从状态栏中移除则简单得多了,NotificationManager.cancel()方法可以提供这一操作,它接受tag、id作为参数用于指明希望移除的通知所具有的意图。

NotificationManagerService会将通知发送到StatusBarManagerService

2. NotificationManagerService中的通知信息

接下来看 NotificationManagerService如何对通知信息进行管理。
参考 NotificationManager Service.enqueueNotificationWithTag()的实现
[NotificationManagerService.java-->NotificationManagerService.enqueueNotificationWithTag()]
public void enqueueNotificationInternal(String pkg,int callinguid,int callingpid,
                                                          String tag,int id,Notification notification,int[] idout,int userId){
    /*①首先安全检查。
      在提交通知信息时提供包名可以用于保证通知信息的安全。checkCallerIsSystemOrSameApp()会获
      取通知信息提交者的UID
并与PackageManager中获取分配给拥有指定包名的应用程序的UID进行比对
      倘若二者不相同,则表示有恶意软件尝试通过冒用包名的方式恶意篡改其他应用程序发出的通知信息
      checkCallexIsSystemorSameApp()会出一个运行时异常以禁止这种行为*/
    checkcallerrsSystemorSameApp(pkg);
    //倘若包名为android,表示通知来自Android系统服务
    final boolean issystemNotification=("android".equals(pkg));
    .......
    //限制每个应用程序最多只能提交50个通知。这是为了防止恶意软件或通过注册大量通知导致系统瘫痪
    if(!isSystemNotification){
        ......
        if(count>=MAX_PACKAGE_NOTIFICATIONS){
            return;
        }
    }
    ......
    /*②接下来为根据通知信息的重要性(即priority字段)对信息进行打分。由于priority的取值为-2、
      -1、0、1      4种,因此score此时的取值范围为-20到10之间。
      随后会根据情况对通知信息的打分结果进行修正。倘若其分数过低,此通知信息便会被忽略
*/
    int score=notification.priority *NOTIFICATION_PRIORITY_MULTIPLIER;
    /*倘若发送者所在的应用程序已经被禁止发送通知,则将此通知信息的分数设置为垃圾分数(JUNK_SCDRE)垃圾
      分数为-1000。默认情况下NotificationManagerService并未启用这一机制。
      系统开发者可以通过将ENABLEBLCCKED_NOTIFICATIONS并通过调用setNotificationEnabledForPackage()
      方法禁止某一应用程序发送通知
*/
    if(ENABLE_BLOCKED_NOTIEICATIONS
                &&!isSystemlotification
                &&!areNotificationsEnabledForPackageInt(pkg)){
        score=JUNK_SCORE;
    }
    //倘若分数过低,则忽略此通知信息
    if(score< SCORE_DISPLAY_ THRESHOLD){
        return;
    }
    //经过打分检查的通知信息都将得到NotificationManagerservice的受理
    synchronized (mNotificationList){
        //③首先会创建一个NotificationRecord实例用于包装所有与此通知相关的信息。
        //包括身份信息(pkg、tag、id、调用者信息、打分以及Notification实例等)

        NotificationRecord r = new NotificationRecord(pkg,tag,id,
                                                    callinguid,callingPid,userId,
                                                    score,
                                                    notification);
        //old表示因为与新通知具有相同的意图而被新通知所替接的NotificationRecord实例
        NotificationRecord old=null;
        //④将新建的NotificationRecord添加到mNotificationList列表中进行保存。
      

        //获取具有相同意图的通知在mNotificationList列表中的位置
        int index=indexofNotificationlocked(pkg,tag,id,userId);
        if(index<0){
            //相同意图的通知不存在则直接将其添加到mNotificationList列表中
            mNotificationList.add(r);
        }else{
            /*若相同意图的通知已经存在,则将已存在的NotificationRecord从列表中删除,然后再将
              新的NotificationRecord加入列表
*/
            old=mNotificationtist.remove(index);
            mNotificationList.add(index,r);
            ......
        }
        /*倘若通知信息用于描述一个正在以前台形式运行的服务,则为其添加FLAG_ONGOING_EVENT以及
          FLAG_NO_CLEAR两个标记,以确保它不能被用户从下拉卷帘中删除
*/
        if((notification.flags&Notification.FLAG_FOREGROUND_SERVICE)!=0){
            notification.flags |= Notification.FLAG_ONGOING_EVENT
                        | Notification.FLAG_NO_CLEAR;
        }
        ......
        /*⑤将通知信息提交给StatusBarManagerService。Notification中的icon字段的值对这里的流
          程起到了决定性的影响。当icon不为0时,表示这是一个有效的通知信息,因而它会被提交给
          StatusBarManagerService
反之则表示这不是一个有效的通知信息,它非但不会被提交给
          StatusBarManagerservice,还会导致StatusBarManagerService中与它具有相同意图通知被删除
*/
        if(notification.icon!=0){
            /*创建一个StatusBarNotification,包装所有与此通知有关的信息。其内容与NotificationRecord
              大同小异。最大的区别是NotificationRecord是通知信息存在于NotificatlonManagerService
             中的形式,而StatusBarNotification则是通知信息存在于StatusBarManagerService中的形式
*/
            final StatusBarNotification n=new StatusBarNotification(
                        pkg,id,tag,r.uid,r.initialpid,score,notification,user);
            if(old!=null &s old.statusBarkey!=null){
                /*倘若具有相同意图的通知信息已存在于NotificationManagerService中,则选择通过
                  updateNotification()对通知进行更新。

                  注意,新的NotificationRecord将继承旧有NotificationRecord的statusBarKey成员
                  的值。statusBarKey是一个Binder实例,用于在StatusBarManagerService中唯一
                  表示一个通知
*/
                r.statusBarKey=old.statusBarkey;
                /*使用新的StatuaBarNotification实例更新r,statusBarkey所对应的位于
                  StatusBarManagerService的通知
*/
                mStatusBar.updateNotification(r.statusBarkey,n);
                ......
            }else{
                /*向statusBarManagerservice提交新建的statusBarNotification实例。Status-
                  BarManagerservice会为此通知创建一个Binder对象,以此作为通知在Status-
                  BarManagerService中的唯一标识。当NotificationManagerService需要对sta-
                  tusBarManagerService中的通知进行更新或划除操作时,必须提供这一唯一标识
                  这个 Binder对象并没有包含任何IPC操作,它存在的目的是在 StatusBarManagerService中
                  唯一地标识个StatusBarNotification实例
                  NotificationRecord会保存这一 Binder对象到它的 statusBarKey
                  成员中,以便与 StatusBarManagerService中的 StatusBarNotification实例建立联系*/

                r.statusBarKey=mstatusBar.addNotification(n);
                ......
            }
        }else{
            //新通知的icon字段为0会导致StatusBarManagerService中的相应通知被删除
            mstatusBar.removeNotification(old.statusBarkey);
        }
        /*当新通知以StatusBarNotification的形式提交给StatusBarManagerService之后,
          Notification会根据新通知的要求进行通知音、震动以及LED指示灯的操作。
不过这与本章所讨论的System-
          UI关系不大,因此这里不再赘述。感兴趣的读者可以自行研究*/
        ......
    }
    idout[0]=id;
}
      可见, NotificationManagerService接收到一则通知信息时,会首先对一些不希望进行显示
的通知进行过滤。这一过滤动作主要分为两个方面
      口  安全性过滤,避免恶意应用通过冒用包名、tag以及id对其他应用程序的信息进行篡改
      口  打分过滤,用于将一些低重要性的,或者由那些位于黑名单中的应用所发送的通知排
            斥在外。
      当一则通知通过安全性以及打分的两层过滤之后, NotificationManagerService会将其封装
为一个 NotificationRecord,并将其添加到 mNotificationList列表中,表示已经接受了这则通知
信息。 NotificationManagerService使用pkg、tag以及id三个信息一并描述通知的意图。新的
通知倘若和现有通知具有相同的意图, NotificationManagerService会将旧有通知从列表中删除,
并将新的通知加入列表。从这个意义上讲,pkg、tag以及id在 NotificationManagerService中
共同构成了一则通知的唯一标识

      自 Android4.2开始, Android开始支持多用户机制。因此目标用户的 userid也构成了
通知意图之一,因为发送给用户A的通知不应影响到发送给用户B的通知。因此准
确地讲构成通知的意图或唯一标识的应该是pkg、tag、id以及 userid 4者共同构成的

不过多用户在最常用的手机设备中并没有被启用。

     在 NotificationManagerService保存 NotificationRecord之后,便开始向 StatusBarManager
Service提交通知的信息,以便通知最终能够显示在状态栏上。 StatusBarManagerService
以 Status BarNotification的形式保存一则通知,其内容与 Notification Record大同小异。
Status BarManagerService会为每一个 Status BarNotification分配一个 Binder对象。这个 Binder
对象并没有包含任何IPC操作,它存在的目的是在 Status Bar ManagerService中唯一地标识
个 Status BarNotification实例。 NotificationRecord会保存这一 Binder对象到它的 statusBarKey
成员中,以便与 StatusBarManagerService中的 StatusBarNotification实例建立联系。
      将通知发送给 Status BarManagerService之后,可以认为通知已经可以显示在状态栏以
及下拉卷帘中了。 NotificationManager Service下一步便是通过 Notification实例中的 sound
vibrate、 ledARGB等字段进行提示操作。至此在 Notification ManagerService中添加通知信息
的阶段完成。
      NotificationManagerService也提供了接口 canceINotification()用于响应 NotificationManager
cancel()方法的调用。在了解了新增一则通知的原理之后,不难想到移除一条通知的流程。
Notification ManagerService首先会从 mNotificationList中找出具有给定意图的 NotificationRecord
将其从列表中删除。然后再将 Notificationrecord中 statusBarKey所标识的 StatusBarNotifi
cation从 StatusBarManagerService中删除。另外, Notification.deletelntent便是在这一过程中
被发送的。

3. StatusBarManagerService中的通知信息

      如前文所述,通知信息由 NotificationManagerService封装为 StatusBarNotification并通过
StatusBarManagerService.addNotification()方法传递给 StatusBarManagerService。本节将讨论
StatusBarManagerService将如何对其进一步处理。
参考 addNotification()方法的实现:
[StatusBarManagerService. java--> StatusBarManagerService.addNotification()]
public IBinder addNotification(StatusBarNotification notification){
    synchronized(mNotifications){
        //①首先创建一个新的Binder对象用作通知的唯一标识
        IBinder key = new Binder();
        //②以key为键,将通知保存在名为mNotifications的一个Hashmap中
        mNotifications.put{key,notification);
        /*倘若mBar不为null,则表示SystemUI中的BaseStatusBar正处在运行状态。
          如7.1.3节所述,mBar就是BaseStatusBar中的CommandQueue的Bp端,
          负责为StatusBarManagerService提供访问BaseStatusBar的渠道
*/
        if(mBar != null){
            try{
                /*③通过mBar.addNotification()方法将StatusBarNotification实例提交给
                  SyatemUI中的BaseStatueBar
*/
                mBar.addNotification(key,notification);
            }catch(RemoteException ex)(......}
        }
        return key;
    }
}
      正如7.1.3节所述, StatusBarManagerService是一个简单到不能再简单的系统服务。它
不过是状态栏的一个代理。它将外界(如 Notification Manager Service)对通知信息的操作
转发给运行于 SystemU进程中的状态栏,并且在本地保存了一个通知信息的副本(mNotifications)
      注意,即便 SystemU因为某种原因崩溃而使得mBar为null, StatusBarManagerService
仍然会接受添加通知信息的请求,为其分配唯一标识并添加到 mNotifications中。因
为当 SystemUI再次成功启动后调用 StatusBarManagerService.registerStatusBar()方法时,
StatusBarManagerService会将 mNotifications中存储的所有通知一并返回给 SystemU中的
BaseStatusBar用于显示
(参考7.1.2节)。因而, StatusBarManagerService的存在使得通知信息
的丢失概率降到最低点。
      相应的更新或移除通知的操作在 StatusBarManagerService中的实现也十分简单。即修
改或移除 mNotifications列表中的 StatusBarNotification,然后再将操作通过mBar转发给
Systemu进程中的 BaseStatusBar。

4.状态栏中的通知信息

      StatusBarManagerService将 StatusBarNotification通过 mBar.addNotification()提交到状态
栏中。状态栏将会为通知信息创建相应的控件,并将其添加到其控件树中。

      接受StatusBarNotification的场所位于 BaseStatusBar.addNotification()方法中,不过由于
BaseStatusBar作为一个抽象类并没有提供 addNotification()的实现,因此需要参考其子类之
的 PhoneStatusBar的 addNotification():
[PhoneStatusBar.java-->PhoneStatusBar.addNotification()]
public void addNotification(IBinder key,StatusBarNotification notification){
    //①通过addNotificatoinViews()将通知的内容添加到控件树中
    StatusBarIconView iconView = addNotificatlonViews(key,notification);
    if(iconView == null)return;//倘若返回使为null则表示添加失败
    ......
    //②为新的通知启动fullscreenIntent或进行ticker
    if(notification.notification.fullscreenIntent != null){
        /*fullscreenIntent拥有比tickerText更高的优先级。因此当fullscreenIntent存在时,将启
          动fullScreenIntent
*/
        try{
            notification.notification.fullScreenIntent.send();
        }catch (PendingIntent.CanceledException e){}
    }else{
        //否则进行ticker动作
        if(mCurrentlyIntrudingNotification == null){
            tick(null,notification,true);
        }
    }
    //③更新周边控件。例如调整下拉卷帘中控件的位置尺寸、设置“清空通知”按钮的可用状态等
    setAreThereNotifications();
    updateExpandedViewPos(EXPANDED_LEAVE_ALONE);

}
      PhoneStatusBar.addNotification()主要分为三个步骤,首先是通过 addNotificationViews()
方法为通知信息创建相应的控件(主要分为通知栏图标以及下拉卷帘中的详细信息两个部分)
然后在 fullscreenIntent以及 tickerText之间选择一个以提醒用户有新通知,最后更新周边的相
关控件。
      本节重点介绍 addNotificationViews()方法如何为通知信息创建相应的控件。
addNotificationViews()的实现位于 BasestatusBar中
[BaseStatusBar.java-->BaseStatusBar.addNotificationViews()]
protected StatusBarteonView addNotificationviews(IBinder key,StatusBarNotification notification){
    /*①首先创建一个类型为StatusBarIconView的控件,它用于显示通知的图标。StatusBarIconView
      的祖父类是ImageView。它为了适应状态栏图标的现实需求进行了一些定制。例如可以在图标之上显示
      一个数字(Notification.number),自动为图标设置level(Notification.iconLevel),以及从通知
      发送者的APK中加载图标资源等。读者将其理解为一个ImageViev即可
*/
    final StatusBarIconView iconView = new StatusBarIconView(mContext,
                                        notification.pkg+"/0x"+Integer.tolexstring(notification.id),
                                        notification.notification);
    iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
    //②创建StatusBarIcon实例,用于保存  在Notification实例中   与通知图标相关的信息
    final StatusBarIcon ic = new StatusBartcon(notification.pkg,
                                            notification.user,
                                            notification.notification.icon,
                                            notification.notification.iconLevel,
                                            notification.notification.number,
                                            notification.notification.tickerText);
    //设置statusBarIconView所显示的图标
    if(!iconView.set(ic)){
        return null;
    }
    /*③创建NotificationData.Entry实例
      保存key,StatusBarNotification以及StatusBarIconView。
      NotificationData.Entry是通知信息在状态栏中存在的形式
*/
    NotificationData.Entry entry =
                            new NotificationData.Entry(key,notification,iconView);
    //④创建通知在下拉卷帘中的控件树,并将共添加到mPile中。关于mPile,请参考7.2.1节
    if(!inflateViews(entry,mPile)){
        return null;
    }
    //⑤将保存了新通知的NotificationData.Entry实例保存到mNotificationData中
    int pos = mNotificationData.add(entry);
    /*⑥更新通知在下拉卷帘中的展开状态。一般情况下,所有的通知信息在下拉卷帘中都有一个固定的高度。
      在这一固定高度下通知所能显示的信息十分有限。自Android4.2开始,状态栏允许下拉卷帘中的通知可
      以展开状态。处于展开状态的通知的控件树的TayoutParams.height将会被设置为WRAP_CONTENET而不是一
      个固定高度,发其得以获取充足的空间以显示足够详细的信息。它会使得在
      mNotificationData中最后一个通知处于展开状态(高度为 WRAP_CONTENT),而其
      他通知则处于折叠准状态(高度为一个固定高度)。换句话说,此时处于展开状态的
      是打分最高的通知,或者是最新的通知。
*/
    updateExpansionstates();
    //⑦更新状态栏中所有通知图标的显示状态
    updateNotificationIcons();
    return iconView;
}
addNotificationViews()方法中的内容比较多,总结如下:
      口  创建用于在状态栏中显示图标的类型为 StatusBalconView的控件,并为此控件设置
            需要其显示的图标—----- StatusBarlcon。 StatusBarIcon收集了来自 Notification实例中与
            图标相关的信息供 StatusBarlconView显示。
      口  为新通知创建一个 NotificationData.Entry类型的实例,它保存了 StatusBarlconView控
            件,以及 StatusBarNotification实例。 NotificationData.Entry是通知信息在状态栏中的
            存在形式,并且会被添加到 mNotificationData列表中。mNotificationData并不是一个
            简单的 ArrayList列表,而是一个在其内部维护了一个 ArrayList的 NotificationData类
            的实例。它的add()方法保证所有 Entry都按照其打分( score)的升序排列。由于通知
            在状态栏的图标显示顺序以及在下拉卷帘中的显示顺序都与其在 NotificationData中
            的顺序相关。这也是决定打分结果的 Notification. priority最大的影响所在。另外,倘
            若两个通知的打分相同,则通过 Notification.when,即通知的时间进行排序,可以理
            解为新的通知比旧的通知拥有更高的优先级。

      口  通过 inflateViews()方法为新通知创建用于显示在下拉卷帘中的控件树。并将这个控
            件树放置到 mPile中。此时用户可以通过下拉卷帘看到这条通知

      口  接下来通过 updateExpansionstates()设置通知在下拉卷帘中的展开状态。它会使得在
            mNotificationData中最后一个通知处于展开状态(高度为 WRAP_CONTENT),而其
            他通知则处于折叠准状态(高度为一个固定高度)。换句话说,此时处于展开状态的
            是打分最高的通知,或者是最新的通知。
      口  最后通过 updateNotificationIcons()更新状态栏中所显示的图标列表。为什么不简单
            地将新建的 StatusBarIconView添加到 mNotificationIcons(参考7.2.1)中呢?因为
            图标在状态栏中的显示顺序依赖于通知在 mNotificationData中的顺序。
            updateNotificationlcon()会将待显示的 StatusBarlconView按照其对应的通知在 
            mNotificationData中的顺序进行排序,使得具有最高打分或最新通知图标的已显示在状
            态栏的最左侧。至于为什么显示最左侧的,则是因为靠近右侧的图标有可能会因
            NotificationData的宽度不足以显示全部通知图标而变得不可见。

接下来讨论 inflateViews()方法的实现,因为它解释了通知在下拉卷帘中的控件树结
构如何。了解了通知在下拉卷帘中的控件树便可以解释为什么通知可以拥有折叠与展开两
种状态了。如7.2.2节所述,通知的发送者通过 Notification.contentView以及 Notification.
bigContentView两个 RemoteView定义通知在下拉卷帘中的显示内容。因此 inflateView()的重
点是如何将这两个 RemoteView放置在mPile中,以及如何在这两者之间做出选择。
参考其实现:
[BaseStatusBar.java--> BaseStatusBar.inflateViews()]
protected boolean inflateViews(NotificationData,Entry entry,ViewGroup parent){
    //通知在下拉卷帘中的最小高度为64dp
    int minHeight = mcontext.getResources()
                    .getDimensionPixelSize(R.dimen.notification_min_height);
    //通知在下拉卷帘中的最大高度为256dp
    int maxHeight=mContext.getResources{)
                    .getDimensionPixelsize(R.dimen.notification max height);
    StatusBarNotification shn = entry.notification;
    /*①首先获取Notification实例中的contentView以及bigcontentView。
      它们分别被保存于oneU以及large之中
*/
    RemoteViews oneU = sbn.notification.contentView;
    RemoteViews large = sbn.notification.bigcontentView;
    if(oneU == null){return false;}//contentView是必要的
    LayoutInflater inflater = (LayoutInflater)mContext.getSystemservice(Context.LAYOUT_INELATER_SERVICE);
    /*②使用LayoutInflater从layout资源中创建一棵控件树,并保存在row中。
      这棵控件树就是通知在下拉卷帘中的根控件
。注意row直接被创建到mPile之中*/
    View row = inflater.inflate(R.layout.status_bar_notification_row,parent,false);
    ......
    /*③从模板中获取两个ViewGroup,分别是content和adaptive。其中content是adaptive的父控
      件,主要用于接受用户的触摸事件,以便在用户点击时启动Notification.contentIntent。
      而adaptive则用来容纳oneU和large所描述的控件树
*/
    ViewGroup content = (ViewGroup)row.findViewById(R.id.content);
    ViewGroup adaptive = (ViewGroup)row.findViewById(R.id.adaptive);
    //④为content设置OnClickListener。当用户点击被content消费时将会触发contentIntent
    pendingIntent contentIntent-sbn.notification.contentIntent;
    if (contentIntent!=null){
        final View.onClickListener listener = new NotificationClicker(contentIntent,
                                            sbn.pkg,sbn.tag,sbn.id);
        content.setOnClickListener(listener);
    }else{
        content.setonclickListener(null);
    }
    //⑤创建由Notification.contentView/bigContentview所描述的控件树
    View expandedoneU = null;
    View expandedLarge = null;
    try{
        //从oneU(Notification.contentView)中创建出控件树并保存在expendedOneU
//它是通知处于折叠状态时所显示的内容。它存储在 NotificationDataEntry. expanded中

        expandedoneu = oneU.apply(mContext,adaptive,monclickHandler);
        if(large!=null){
            /*如果large(Notification.bigcontentView)不为null,
              则创建出一个控件树并保存在expendedLarge中
它是处于展开状态时所显示的内容,存储在 NotificationDataEntry. expanded中。*/

            expandedLarge = large.apply(mContext,adaptive,monClickHandler);
        }
    }catch (RuntimeException e){......}
    //⑥从Notification.contentView所创建的控件树被添加到adaptive中
    if {expandedoneU!=null){
        SizeAdaptiveLayout.LayoutParams params =
                            new SizeAdaptivelayout.LayoutParams(expandedoneU.getLayoutParams());
        //注意LayoutParams中的最小高度和最大高度都被设置为minHeight
        params.minHeight=minHeight;
        params.maxHeight-minHeight;
        adaptive.addView(expandedOne0,params);
    }
    //⑦从Notification.bigcontentview所创建的控件树被添加到adaptive中
    if(expandedLarge!=null){
        SizeAdaptiveLayout.LayoutParams params=
                        new SizeAdaptiveLayout.LayoutParams(expandedLarge.getLayoutParams());
        //注意LayoutParams中的最小高度被设置为minHeight+1,而最大高度被设置为maxHeight
        params.minHeight = minHeight+1;
        params.maxHeight = maxHeight;
        adaptive.addView(expandedLarge,params);
    }
    ......
    //设置通知是否支持展开状态。依据是Notification是否提供了bigContentView
    row.setTag(R.id.expandable_tag,Boolean.valueOf(large != null));
    //将本方法所创建的控件树保存到Notification.Entry中
    entry.row = row;
    entry.content = content;
    entry.expanded = expandedoneU;
    entry.setLargeView(expandedLarge);
    return true;
}
inflateViews()方法为通知创建了如下5个重要的控件。
口  row:通知在下拉卷帘中的根控件,除此之外并没有什么特殊性。它存储在 Notification-
      DataEntry row中。
口  content:根控件下的一个子 ViewGroup。它通过 OnClickListener监听那些没有被其子
      控件所消费的用户点击事件,并在点击到来时出发 Notification.contentintent。它存储在NotificationData.Enry.content中。
口  adaptive: content的一个子 ViewGroup,作为 Notification. contentView以及 Notifi
      cation.bigContent View的父控件,它的类型是 Size AdaptiveLayout
口  expandedoneU:从 Notification.contentView中创建出来的控件树。它是通知处于折叠
      状态时所显示的内容。它存储在 NotificationDataEntry. expanded中。
口  expanded Large:从 Notification. bigContentView中创建出来的控件树。它是处于展开
      状态时所显示的内容,存储在 Notification Data Entry. expanded中。

      这里面似乎并没看到对 contentView 以及 bigContentView进行选择的代码。不过,注意将它
们添加到 adaptive时对 LayoutParams.minHeight/max Height所做的设置,能发现一些有趣的事情
contentView的 LayoutParams.minHeight/maxHeight被设置为 minHeight(26dp),而 bigContentView
的 LayoutParams.minHeight/maxHeight分别被设置为 minHeigh26dp)+1, maxHeight256dp)
也就是说, bigContentView的最小高度也比 contentView的最大高度要高。
      倘若参考一下它们的父控件 SizeAdaptiveLayout的 selectActiveChild()方法,可以发现它
会根据 onMeasure()时所提供的MeasureSpec在子控件中寻找一个尺寸合适的控件作为
ActiveChild。这个 ActiveChild会在 onLayout()时被设置为 VISIBLE,而其他的子控件则会被设置
为GONE。因此,当给予 adaptive的高度足够时,将会选择 bigContentView作为其显示内容
而当高度不足以显示 bigContentView时,则会将 contentView作为其显示内容。

      参考 BaseStatusBar中用于展开一则通知的 expandView()方法的实现:
protected boolean expandView(NotificationData.Entry entry,boolean expand){
    //rowHeight为26dp,与inflateView()中的minHeight一致。
    int rowHeight = mcontext.getResources()
                    .getDimensionPixelsize(R.dimen.notification_row_min_height);
    ViewGroup.LayoutParams lp = entry.row.getLayoutParams();
    if (entry.expandable()&& expand){
        /*①设置row的高度为WRAP_CONTENT。这使得它能够尽可能地给予adpative足够的高度空间以容
          纳其子控件。在这种情况下bigContentView将极有可能被选择为Active child。此时通知将处于
          展开状态
*/
        lp.height=ViewGroup.LayoutParama.WRAP_CONTENT;
    }else{
        /*②设置row的高度为26dp。与contentView的高度相同,却小于bigContentView的最小高度。
          此时adaptive必定会选择contentView作为Active child。此时通知将处于折叠状态
*/
        lp.height=rowHeight;
    }
    entry.row.setLayoutParams(1p);
    return expand;
}
可见由于 SizeAdaptive Layout的帮助,设置通知在下拉卷帘中的展开与折叠十分简单

      当row的 LayoutParams.height被设置为 WRAP_CONTENT时,并不一定会选择
bigContentView作为 Active Child,因为 WRAP_CONTENT并没有给予子控件完全无限制
的尺寸。从理论上讲,倘若row的父控件无法给予大于26p的高度空间,则 adaptive
仍然会显示 contentView而不是 bigContentView,只是这种情况实在少见。


总结状态栏接受一则通知信息的关键信息如下:
      口  关于状态栏上的通知图标。状态栏将 Notification中与图标相关的信息封装在一个
            Statusbarlcon中,其中包括 Notification下的icon、 iconLevel、 number等字段。
            StatusBarlcon会交给 StatusBarIconView控件显示。
      口  关于下拉卷帘中的通知信息。状态栏从 R.layout.status_bar_notification_row中创建
            棵控件树作为模板。在这棵控件树中, R.id.content用于接受用户的点击事件,并
            在被点击时触发 Notification.contentIntent。R.id.adaptive则用来容纳 Notification
            .contentView以及 Notification.bigContentView。 R.id.adaptive会在测量时根据可用的高
            空间在 contentView以及 bigContentView二者之间做出选择。
      口  通知的 StatusBarIconView会作为 mNotificationIcons的子控件显示在状态栏上。而通
            知的 R.layout.status_bar_notification_row则会作为mPile的子控件显示在下拉卷帘中。
      口  通知的所有信息包括 StatusBarNotification以及通知所对应的控件 StatusBarIconView、
            R.layout.status_bar_notification_row、 contentView以及 bigContentView,都会被保存在
            NotificationData.Entry中。因此 NotificationData.Entry是通知在状态栏中存在的形式
      口  所有的 NotificationData.Entry按照其打分以升序保存在 NotificationData中。 Notification.
            Data.Entry在 NotificationData中的顺序决定了通知在状态栏以及下拉卷帘中的显示顺序。
5.总结
      本节完整地讨论了通知信息发送者从 NotificationManager.notify()起到通知信息被显示
在状态栏及其下拉卷帘中的过程。
通知信息先后经历了通知发送者、 NotificationManagerService、
Status BarManagerservice、状态栏 4个参与者,最终呈现在用户面前。
      通知发送者负责定制 Notification实例,以描述通知信息的内容及行为。在这里通知信息
存在的形式是 Notification,并且以tag及id作为通知信息的唯一标识

      NotificationManagerService负责对通知信息进行安全性检查并按照其 priority进行打分。
打分的结果将决定通知信息在状态栏中的显示顺序,过低的打分甚至会导致此通知信息被忽
略。在 NotificationManagerService中,通知信息存在的方式是 NotificationRecord,被保存在
mNotificationList列表中,并且通知发送者的包名pkg、tag以及id是通知信息的唯一标识。
新的通知信息会替换具有相同唯一标识的现有通知信息。

      StatusBarManagerService负责将通知提交给状态栏。通知信息在这里的存在方式是
StatusBarNotification,并以一个 Binder实例作为其唯一标识。

      状态栏负责将通知信息显示给用户。通知信息在这里的存在方式是 NotificationData.
Enty
,其中保存了 StatusBarNotification,以及负责显示的控件。其中通知图标由StatusBalconView
显示在 mNotificationIcons中,而通知的详细信息则由 R.layout.status_bar_notification_row
显示在 mPile中。

7.2.3系统状态图标区的管理与显示

      系统状态图标区是指状态栏右侧的用于显示一系列用于指示系统状态的图标的区域,用
于提示用户系统的当前状态。闹钟、同步等指示图标都显示在这一区域中。
从表现形式上来
看它与通知图标并无区别,只是状态栏对这些图标的意图进行了严格限定。
      就通知的发送者而言,通知的意图由tag与id定义,而 NotificationManagerService以及
状态栏并不关心意图是什么而一律准予显示。系统状态图标区的图标意图由一个字符串描述。
StatusBarManagerService维护了一个准许显示在系统状态区的预定义的意图列表,这个列表
由 frameworks/base/core/res/res/values/config. xm中的字符串数组资源 config_statusBarlcons定
义。 StatusBarManagerService会拒绝使用者提交上述预定义的意图之外的图标。
读者可以参考
config_statusBarlcons的内容了解系统通知区可以显示的意图。

      虽说config_statusBarlcons中定义了 phone_signal,battery,clock等意图,不过用户所
见的信号图标、电量状态及系统时钟并不属于系统状态图标区。它们由 SystemU中的
SignalClusterBatteryController以及 Clock单独维护。
请参考7.2节开篇所述的内容。


      尽管对系统图标的意图限定十分严格,不过系统图标的设置者可以为某个意图设置任意
图标。

1.在系统状态图标区显示图标的方法

在系统状态图标区中显示一个图标可以使用 StatusBarManager.setIcon()方法完成。这一方
法需要以下4个参数:
口  slot,一个字符串,用于声明图标的意图。它必须存在于 config_statusBarIcons所预定
      义的意图列表之中。

口  icon,一个用于显示的图标资源id。
口  iconLevel,指出图标资源的level。
口  contentDescription,一个字符串,用于详细描述图标的含义。
Status Bar Manager, setlcon()方法的实现如下
[StatusBarManager.java->StatusBarManager.setlcon()]
public void setcon(String slot,int icontd,int iconmevel,string contentpescription){
    try{
        final IStatusBarservice svc = getService();
        if(svc != null){
            //接将settcon请求转发给StatusBarManagerService
            svc.setIcon(slot,mContext.getPackageName(),iconId,icontevel,contentDeacription);
        }
    }catch (RemoteException ex){.....}
}

2. StatusBarManagerService对系统状态图标的管理

首先参考 StatusBarManagerService.setIcon()方法的实现:
[StatusBarManagerService.java-->StatusBarManagerService.setlcon()]
public void setIcon(String slot,String iconPackage,int iconId,int iconLevel,string contentDescription){
    /*①首先是安全性检查。既然系统状态图标用于表示系统状态,因此必须限定图标设置者的身份。图标
      设置者必须拥有签名级系统权限android.permission.STATUS_BAR才能设置系统状态图标
*/
    enforceStatusBar();
    synchronized (mIcons){
        /*②从mIcons中获取意图的索引。mIcons中存储了所有预定义的意图列表。因此,倘若没能找到
          给定意图的索引,则说明这不是预定义的意图之一。StatusBarManagerService将会抛出一个
          异常终止图标设置工作
*/
        int index=mIcons.getSlotIndex(slot);
        if(index<0){
            throw new SecurityException("invalid status bar icon slot:"+slot);
        }
        /*③创建一个StatusBarIcon,用于封装与图标相关的信息。注意,系统状态图标使用了和通知图
          标一样的数据结构StatusBarIcon,因此,可以自然而然地想到系统图标的显示同样使用了用于显
          示通知图标的StatusBarrconView
*/
        StatusBarIcon icon = new StatusBarIcon(iconPackage,Userlandle.ONER,iconId,
                                            iconLevel,0,
                                            contentDescription);
        //④将新的StatueBarIcon保存到mIcons中
        mIcons.setIcon(index,icon);
        //⑤将新的StatuaBarIcon提交给SyatemUI中的状态栏

        if(mBar != null){
            try{
                mBar.setIcon(index,icon);
            }catch(RemoteException ex){......}
        }
    }
}
      设置系统状态图标需要一个签名级系统权限 android. permission. STATUS_BAR。这说明
StatusBarManagerService对图标的设置者的身份限制十分严格。不仅仅是由于系统状态图标指
示了系统级的状态因而需要对图标的来源足够信任,还因为系统状态图标区对状态栏的空间
具有优先占用权(参考7.2.1节),过多的系统状态图标会挤压通知图标的可用空间从而影响用
户的体验。因此 StatusBarManagerService必须确保设置者是值得信任的
      mIcons是 StatusBarlconList类的实例,用于保存系统图标的列表。当 StatusBarManagerService
初始化时会通过 StatusBarlconList.defineSlots()方法使用 config_statusBarlcons所定义的意
图列表对 StatusBarlconsList进行初始化。
初始化后的 StatusBarlconsList中会包含两个数组
mSlots以及mIcons(注意不是 StatusBarManagerService.mIcons)。其中 mSlots数组存储了预定义
的图标意图,而 mIcons存储了某一意图下所显示的图标信息 StatusBarlcon,二者通过数组索引
建立关联。因此 StatusBarManagerService可以通过 StatusBarlconList.getSlotIndex()检查给定的
意图是否处于预定义意图列表中。

      StatusBarManagerService会为新图标创建一个 StatusBarIcon实例用于封装与图标相关的
信息,这一数据结构同样被用于存储通知图标的信息。新的 StatusBalcon实例会被保存在
mIcons( StatusBarlconList)中。其在 mIcons中的索引与图标意图在 mSlots中的索
引相同,以保证二者的对应关系。
      StatusBarIconList的特点使得将系统状态图标的意图称为slot变得十分形象。当
mSlots的每个元素都存储一个预定义的意图之后, mIcons数组中每个索引位置便成为
对应意图的插槽。

      最后新的 StatusBarlcon会通过 mBar.addIcon()方法提交给 SystemUI中的状态栏。

3.在状态栏的系统图标区显示图标

      流程又来到 SystemUI的 CommandQueue。 CommandQueue中与 StatusBarManagerService
一样保存了一个StatusBarIconList的实例 mList。 CommandQueue会检查给定的意图(这时的意
图已经从一个字符串转换为意图在 StatusBarIconList中的索引)在 mList中是否已经存在一个
StatusBarlcon。倘若不存在则会通过调用 BaseStatusBar. addlcon()方法添加一个图标,否则通
过 BaseStatusBar.updatelcon()更新图标。

      由于 BaseStatusBar并没有提供 addIcon()方法的实现,因此需要从其子类中寻找这个方
法。以 PhoneStatusBar为例,参考其代码:
[PhoneStatusBar.java-->PhoneStatusBar.addIcon()]
public void addIcon(String slot,int index,int viewIndex,statusBarIcon icon){
    //创建一个用于在状态栏中显示图标的StatusBarIconView
    StatusBarIconView view = new StatusBarIconView(mContext,slot,null);
    //设置StatusBarIcon
    view.set(icon);
    /*将新的StatusBarrconView添加到mStatusIcons中。mStatusIcons就是在状态栏中构成系统状态
      图标区的ViewGroup。
      其中的viewIndex参数由图标意图在StatusBarIconList.mSlots中的索引产生。详情请参考
      StatusBarList.getViewIndex()方法
*/
    mStatusIcons.addview(view,viewIndex,
                        new LinearLayout.LayoutParams(mIconSize,mIconsize));
}
updatelcon()方法的实现也与之类似:
public void updateIcon(String slot, int index, int viewIndex,
                        StatusBarIcon old, StatusBarIcon icon) {
    //根据 viewIndex从系统图标区中获取用于显示此意图图标的 StatusBarIconView
    StatusBarIconview view = (statusBarIconview)mStatusIcons.getchildAt(viewIndex);
    //更新 StatusBarIconView的显示内容
    view.set(icon);

}
      可见系统状态图标的显示相较于通知的显示来说简单得多。关于 mSatusBarlcons的介绍
参考7.2.1节。

4.系统状态图标的主要设置者—— PhoneStatusBarPolicy

      尽管StatusBarManager提供了设置系统图标的接口,不过这一接口的使用者并不多。绝
大多数系统状态图标都是被一个名为 PhoneStatusBarPolicy类进行设置的。

      在7.1.2节所介绍的 PhoneStatusBar.start()方法的最后, PhoneStatusBarPolicy被创建并且
保存在 PhoneStatusBar.mIconPolicy中。

      PhoneStatusBarPolicy的工作原理与7.2节开篇所介绍的 NetworkController、 BatteryController
等组件一样,通过监听一系列与系统状态相关的广播,并在这些广播到来之时通过调用
StatusBarManager.setIcon()接口修改系统状态图标。

PhoneStatusBarPolicy所监听的广播列表如下:
      口  Intent.ACTION_ALARM_ CHANGED。
      口  Intent.ACTION SYNC_STATE CHANGED。
      口  AudioManager.RINGER_MODE_CHANGED_ACTION。
      口  BluetoothAdapter.ACTION_STATE _CHANGED。
      口  BluetoothAdapter.ACTION CONNECTION_ STATE CHANGED。
      口  Telephonylntents.ACTION_SIM_STATE_CHANGED。
      口  TtyIntent.TTY_ENABLED_CHANGE_ACTION。

在某一广播到来时, PhoneStatusBarPolicy会根据广播的类型分别调用 updateAlarm()、
updateSyncState()、 updateBluetooth()、 updateVolume()、 updateSimState()、 updateTTY()对特定
意图的系统图标进行设置。

5.总结

      状态栏系统状态图标区的管理与显示比较简单。读者在理解如何设置系统状态图标之外,
还应当理解如何扩充预定义的图标意图列表。简单来说,扩充位于 frameworks/base/core./resf
res/values/config. xml中的 config status Barlcons即可。

7.2.4状态栏总结

      本节主要介绍状态栏中两种类型信息的管理与显示的原理,即通知信息以及系统状态图
标。其他三种信息(电量信息、信号信息以及系统时钟)的实现相对简单,读者可以以本节的
介绍为引导自行研究。
      另外,状态栏的行为与现实还受到一个重要信息的影响,即 SystemUIVisibility。由于
它同时还影响了导航栏的显示与行为。因此本章将在完成导航栏的介绍之后再对 SystemU
Visibility进行讨论。

7.3深入理解导航栏

      导航栏是指显示在屏幕底端或右端容纳了一排虚拟按键的一个窗口。导航栏之所以存在
有两个目的:

      口  通过提供虚拟按键作为物理键的替代品。其中最常用的是BACK、HOME以及
            RECENT这三个键。
      口  可以为某些行为提供最快捷的入口。因为导航栏是常驻屏幕的窗口,因此导航栏是启
            动某些行为最快捷也是最方便的场所。日前 Android在导航栏上提供了快速启动搜索
            的入口。

      导航栏的存在占用了屏幕的一块显示区域。尽管很多用户对此颇有微词,然而在移动设
备的屏幕尺寸越来越大的情况下,导航栏所能提供的好处已远远超过了它所占用的那一块小
小的空间的价值。导航栏取代物理按键无疑节约了设备的制造成本,而更重要的是,由于软
件永远比硬件灵活,导航栏能够通过增加或删除其上的虚拟按键以适应不同的应用场景。例
如导航栏可以仅为拥有选项菜单的应用显示菜单键,也可以为不能使用BACK键的应用隐藏
BACK键。另外,导航栏还可以作为快速启动某些行为的入口。因此基于导航栏的用户体验
大有文章可做,这是物理按键所不能比拟的。

      在平板电脑等大屏幕设备上是没有独立的导航的。这些设备上所拥有的是集成了状
态栏与导航栏的系统栏。不过除了表现形式上的差异之外,实质的工作原理是类似
的。本节所讨论的是应用于小尺寸屏幕上的独立导航栏。读者可以类比地对系统栏进
行研究。

7.3.1导航栏的创建

      同状态栏一样,导航栏的窗口也是通过WindowManager.addView()方法进行创建的。
此讨论导航栏的创建需要留意两个内容,即导航栏控件树的创建,以及导航栏窗口的创建两
部分。前者可以揭示导航栏存在那些功能,以及这些功能的显示方式,而后者则可以揭示导
航栏的窗口特点。

1.导航栏控件树的创建与结构

      作为小屏幕设备特有的组件,导航栏控件树的创建很自然地位于 PhoneStatusBar之中。
PhoneStatusBar的 makeStatusBarView()方法不仅创建了状态栏的控件树,同时也创建了导航
栏的控件树。参考如下代码
[PhoneStatusBar.java-->PhoneStatusBar.makeStatusBarView()]
protected PhoneStatusBarView makeStatusBarView(){
    ......//创建状态栏控件树的代码
    try{
        //①首先向WMS询问是否需要导航栏
        boolean showNav = mWindowManagerService.hasNavigationBar();
        if(showNav){
            //②从R.layout.navigation_bar中创建导航栏的控件树
            mNavigationBarView =
                (NavigationBarView)View.inflate(context,R.layout.navigation_bar,null);
            //设置在导航栏中被禁用的功能
            mNavigationBarView.setDisabledFlags(mDisabled);
            //将PhoneStatusBar设置给导航栏,以便导航栏可以向状态栏查询一些状态
            mNavigationBarView.setBar(this);
        }
    }catch(RemoteException ex){......}
}
      是否创建导航栏的控件树取决于WindowManagerService.haslavigationBar()。这一方法的返
回值取决于PhoneWindowManager中的mHasNavigationBar 成员的取值。
与mHasSystemNavBar
类似,mHasNavigationBar的设置位于PhoneWindowManager.setlnitialDisplaySize()之中。
mHasNavigationBar为true的前提是不使用系统栏,即mHasSystemNavBar为false。之后
mHasNavigationBar的值取决于位于frameworks/base/core/res/res/values/config.xml中的
config_showNavigationBar的取值。
对拥有物理按键的设备来说,可以将config_showNavigationBar
设置为false,从而避免创建导航栏。简单来说,对短边大于720dp的设备来说不需要导航
栏,而对小于720dp的设备来说,是否需要导航栏取决于config_showNavigationBar的设置。

      在屏幕短边小于720dp但是大于600dp的设备上,导航栏会固定在屏幕的底部,而对
短边在600dp以下的设备来说,导航栏的位置会随着设备的方向而改变。在这种情况
下,如果屏幕处于竖直状态(portrait)则导航栏位于屏幕的底部,如果屏幕处于水平
状态(landscape)则导航栏会位于屏幕的右侧。这是因为屏幕处于水平状态时底部的
导航栏会占用本不宽裕的高度。

      导航栏的控件树来源于R.layout.navigation_bar。它的根控件类型为NavigationBarView,并
且被保存在PhoneStatusBar.mNavigationBarView中。
      之后PhoneStatusBar向NavigationBarView传递了两个信息——禁用功能列表mDisabled
以及PhoneStatusBar自身。mDisabled通过按位与的方式存储了一系列被禁用的功能,
NavigationBarView会据此隐藏某些虚拟按键或禁止用户通过滑动手指启动搜索界面。

PhoneStatusBar 则被NavigationBarView用来查询与状态栏相关的一些状态。NavigationBar需
要通过这些状态决定是否允许用户通过在导航栏上通过滑动手指启动搜索界面。
需要禁止启
动搜索界面的状态主要有:此操作被mDisabled所禁止、状态栏的下拉卷帘正在显示(这种情
况下在导航栏上滑动手指被用来关闭下拉卷帘),以及设备尚未完成初始设置。

      在PhoneStatusBar.makeStatusBarView()方法所揭示的这些关于导航栏的信息中,最重要
的当属R.layout.navigation_bar所描述的控件树。R.layout.navigation _bar定义在frameworks/
base/packages/SystemUI/res/layout/navigation_bar.xml中。
参考这个文件的内容,可以发现
NavigationBarView是其根控件,并且有趣的是在这一根控件之中定义了两套导航栏的控件
树——以水平方式进行布局的@id/rot0,以及以垂直方式进行布局的@id/rot90。
除了布局方
向有所差异之外,二者所包含的内容完全一致。其中@id/rot0是导航栏位于屏幕底部时所使
用的控件树,而@id/rot90则是导航栏位于屏幕右侧
时所使用的控件树。
由于二者的内容完全一样(包
括子控件的id),因此导航栏可以根据其显示位置在
二者之间进行无缝切换。
无论是水平布局的R.id.rot0还是垂直布局的
R.id.rot90,它们的控件树结构如图7-5所示。

《深入理解Android 卷III》第七章 深入理解SystemUI(完整版)_第1张图片
口  @id/nav_buttons,一个LinearLayout。其内
      部包含了4个类型为KeyButonView的子控
      件:@id/back@id/home@id/recent_apps
      和@id/menu。它们分别对应虚拟的BACK
      键、HOME键、RECENT键以及MENU键。

口  @id/lights_out,一个LinearLayout,覆盖在
      @id/nav _buttons 之上
(因为@id/rot0以及@id/rot90是FrameLayout),并且在其中拥
      有三个ImageView。这些ImageView都用来显示@drawable/ic_sysbar_lights_out_dot_large
      所描述的一个小型的灰色圆点。@id/lights_out在绝大多数情况下都处于不可见
      状态。
同状态栏一样,导航栏也有低辨识度模式。处于低辨识度模式下的导航栏会将
      @id/lights_out 设为可见并隐藏@id/nav_buttons。此时的导航栏显示为三个不明显的
      灰色圆点,以降低对用户视线的干扰。

口  @id/search_light,一个KeyButtonView,覆盖在@id/lights_out之上并水平方向居中
      显示(在@idrot90中为垂直居中),并且在绝大多数情况下都是不可见的。
它存在的
      意义是当HOME键被禁用之后,倘若通过滑动手指启动搜索界面的功能没有被禁用,
      则将这个KeyButtonView设置为可见,用于提示用户搜索功能仍然可用。

 @id/deadzone,一个DeadZone类型的控件,覆盖于其他所有控件之上。它存在的意
      义在于避免用户的误操作。
由于导航栏紧邻着应用程序的窗口,于是当用户点击应用
      程序中靠近导航栏位置的界面元素时便会有概率误触导航栏上的虚拟按键(想象一下
      当用户在一个应用程序中花费半天时间填写一份表格之后不小心按了BACK键后有
      多恼火吧)。由于DeadZone的存在并且位于虚拟按键之上,用户的触摸事件会首先被
      DeadZone 接收。DeadZone会将那些过于接近导航栏边缘的触摸事件当作用户的误操
      作,并将其消费掉,从而避免触摸事件派发给虚拟按键。

至此相信读者对导航栏控件树的相关信息已经有所了解。接下来讨论导航栏窗口的创建。

2.导航栏窗口的创建

创建导航栏窗口的时机位于7.1.2节所介绍的PhoneStatusBar.start()方法中。参考其代码:
[PhoneStatusBar.java-->PhoneStatusBar.start()]
public void start(){
    ......
    /*BaseStatusBar.start()方法会调用PhonestatusBar.makestatusBarview()
      因此导航栏控件树的创建在这里完成
*/
    super.start();
    //创建导航栏的窗口
    addNavigationBar();
    ......
}
进一步,参考addNavigationBar()的实现:
[PhoneStatusBar.java-->PhoneStatusBar.addNavigationBar()]
private void addNavigationBar(){
    /*偏若mNavigationBarView为null则表示makeStatusBarView()由于WMS.hasMavigationBar()的返
      回值为false,即系统不需要导航栏。因此跳过导航栏窗口的创建
*/
    if(mNavigationBarView == null) return;
    /*① prepareNavigationBarview()负责为NavigationBarView中的虚拟按键(KeyButtonView)设置
      用于响应用户触摸事件的监听器OnClickListner或OnTouchListner。这些监听器将用来产生并向输入
      系统注射虚拟的按键事件。
      另外,这里还在前文所述的@id/rot0以及@id/rot90两棵控件树之间做出选择
*/
    prepareNavigationBarview();
    //②将mNavigatlonBarView作为根控件创建导航栏的窗口
    mWindowManager.addView(mNavigationBarView,getNavigationBarLayoutParams());
}

      在使用mNavigationBarView作为根控件创建导航栏窗口之前,PhoneStatusBar 首先通
过prepareNavigationBarView()方法对NavigationBarView进行一些准备工作。其中包括为虚
拟按键设置监听器,以及在@id/rot0与@id/rot90之间做出选择。这些内容将在7.3.4节介
绍。此刻最值得关注的信息是getNavigationBarLayoutParams()会为NavigationBar创建怎样的
LayoutParams。

      navigation_bar.xml中还定义了@id/rot270,用作导航栏处于屏幕左端时的控件树。不
过目前导航栏永远位于屏幕右端,所以@id/rot270并没有被使用。

[PhoneStatusBar.java->PhoneStatusBar.getNavigationBarLayoutParams()]
private WindowManager.layoutParams getNavigationBarLayoutParams(){
    WindowManager.LayoutParams lp=new WindowManager.Layout Params(
                //①导航栏的宽度与高度都是MATCH_PARENT
                LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT,
                //②类型是TYPE_NAVIGATION_BAR
                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
                0
                | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE//不接受按键事件
                | windowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL//不阻挡位于其下的
                //窗口获取点击事件

                //③当用户在其他窗口上点击时可以收到通知
                | windowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
                PixelFormat.OPAQUE);
    ......
    lp.setTitle("NavigationBar");//窗口名称
    lp.windowAnimations = 0;//不使用窗口动画
    return lp;
}
getNavigationBarLayoutParams()方法为导航栏所创建的LayoutParams与状态栏比较相似,
其中值得关注的不同之处有以下三处。
      首先,导航栏窗口的类型是TYPE_NAVIGATION_BAR。PhoneWindowManager在
windowTypeToLayer()方法中为此类型的窗口分配了非常高的layer值——19,因而它将
显示在绝大多数窗口之上。
为了在小屏幕设备中使导航栏能够在屏幕底端和右端之间
移动,PhoneWindowManager为导航栏创建了下文所述的特殊布局策略,而且Phone-
WindowManager 会根据SystemUIVisibility的设置对导航栏的窗口进行隐藏或显示操作。因此
PhoneWindowManager需要保存导航栏的WindowState以便进行这些操作。TYPE_NAVIGA-
TION _BAR是PhoneWindowManager 识别导航栏WindowState的唯一依据。
      另外,导航栏的宽度与高度都是MATCH_PARENT。读者可能会比较好奇,如此设置
导航栏岂不会充满整个屏幕?回顾第4章所介绍的与窗口布局相关的内容可知,将窗口的
尺寸设置MATCH_PARENT并不是充满整个屏幕,而是充满那个由PhoneWindowManager
为此窗口所提供的ParentFrame。
导航栏的ParentFrame的计算是在PhoneWindowManager.
beginLayoutLw()中完成的
。参考相关代码:
[PhoneWindowManager.java->PhoneWindowManager.begineLayoutLw()]
public void begintayoutLw(boolean isDefaultpisplay,
            int displaywidth,int diaplayHeight,int displayRotation){
    .......
    //倘若存在导航栏的窗口
    if(mNavigationBar!=null){
        /*①首先选择导航栏所在的位置。
          mNavigationBarCanMove决定了导航栏的位置是否可以在屏幕底端和右端之间移动。它是在前文
          所述的PhoneWindowManager.setInitialDisplaysize()方法中进行设置。当屏幕的短边尺
          寸小于600dp时,mNaviqationBaronBottom为true,即可以移动。否则为false,即导航栏
          会被固定在屏幕的底端。

          在mNaviqationBar为true的情况下,倘若设备的宽度小于高度,则显示在屏暮低端,否则显示
          在屏慕的右端。换句话说,在导航栏可以移动的情况下,它永远显示在屏幕的短边之上
*/
        mNavigationBarOnBottom = 
                        (!mNavigationBarCanMove || displaywidth < displayHeight);
        //②计算导航栏的ParentPrame
        if(mNavigationBaronBottom){
            int top = displaylleight - mNavigationBarHeightForRotation[displayRotation];
            /*当导航栏位于屏幕底端时,其ParentFrame是一个高度为
              mNavigationBarHeightForRotation的位于屏幕底部的矩形
*/
            mTmpNavigationFrame.set(0,top,displaywidth,displayHeight);
        }else{
            int left = displaywidth - mNavigationBarwidthForRotation (displayRotation];
            /*当导航栏位于屏幕右端时,其ParentFrame是一个高度为
            mNavigationBarWidthForRotation的位于屏幕右侧的矩形
*/
            mTmpNavigationFrame.set(left,0,displaywidth,displayHeight);
            .......
        }
        ......
        /*③最后对导航栏进行布局。可见不止ParentFrame,导航栏的DisplayFrame、ContentFrame以及
          VisibleFrame都是mTmpNavigationFrame
*/
        mNavigationBar.computeFrameLw(mTmpNavigationFrame,mTmpNavigationFrame,
                                    mTmpNavigationFrame,mTmpwavigationErame);
    }
    .......
}

      PhoneWindowManager对导航栏进行布局时,会首先根据屏幕的宽高确定设备方向为
竖直或水平。当设备处于竖直方向时会在屏幕底部选择一个矩形作为其ParentFrame,而处
于水平方向时会在屏幕右侧选择一个矩形作为其ParentFrame。
由于导航栏LayoutParams
中所设置的尺寸为MATCH_PARENT,因此ParentFrame就是导航栏窗口最终的显示区
域。
计算ParentFrame 过程中所使用的mNavigationBarHeightForRotation 以及mNavigation-
BarWidthForRotation 两个数组也是在PhoneWindowManager.setlnitialDisplaySize()方法中初始
化的。它们的取值来自位于frameworks/base/core/res/res/values/dimens.xml中的navigation_
bar_height
以及navigation_bar_height_landscape两个资源,所以,如果需要调整导航栏的尺
寸,可以从修改这两个资源入手。
导航栏将其窗口宽与高都设置为MATCH_PARENT的目
的就是将自己的尺寸全权交于PhoneWindowManager决定。

      导航栏布局参数中另外一个独特的地方是它声明了FLAG_WATCH_OUTSIDE_TOUCH
标记。声明这一标记的窗口可以在用户点击其他窗口时收到一个名为ACTION_OUTSIDE
触摸事件。在导航栏中,对ACTION_OUTSIDE感兴趣的是上一小节所介绍的DeadZone。
DeadZone用于屏蔽那些发送给导航栏,但是距离导航栏边界很近的触摸事件以防止用户误按
虚拟按键,这相当于DeadZone在导航栏边界附近设置了一个不会响应任何用户操作的死区。

事实上,这一死区的高度(或显示在屏幕右端时的宽度)并不是固定不变的。在一般情况下,
这一死区的高度为12dp(定义于res/values/dimens.xml中的navigation_bar_deadzone_size)。而
当用户点击了导航栏以外的区域使得DeadZone收到ACTION_OUTSIDE之后,死区的高度会
立刻增至32dp(定义于res/values/dimens.xml中的navigation_bar_deadzone_size_max),并在随
后的333毫秒(定义于res/values/config.xml中的navigation_bar_deadzone_decay)中逐渐缩小
至正常状态的12dp。
动态的死区高度降低了用户在操作应用程序的过程中误触导航栏虚拟按
键的概率,因为操作应用程序的过程中ACTION_OUTSIDE事件使得死区高度很大。同时也
保证用户点击虚拟按键时尽可能少地受到死区的影响,因为当用户终止操作应用程序之后死
区的高度会变得很小。

7.3.2虚拟按键的工作原理

      作为物理按键的替代品,维护虚拟按键是导航栏最主要的工作。在第5章介绍
InputDispatcher 时所提供的injectlnputEvent()函数是虚拟按键的实现基础。它会将调用者自制的
一个输入事件加入InputDispatcher的派发队列中并派发,就仿佛这一输入事件来自ImputReader。

InputDispatcher::injectlnputEvent()函数经由InputManager.injectlnputEvent()方法向 java层的使用
者提供调用接口。而导航栏中的KeyButonView就是这一接口的使用者之一。
KeyButtonView
继承自lmageView,它对lmageView最主要的扩展就是根据派发给它的触摸事件转化为按
键事件,并通过InputManager.injectlnputEvent()方法将按键事件注入InputDispatcher的派发
队列。KeyButonView中最重要的字段是mCode,用于指示其生成的按键事件的键值。倘
若没有为KeyButtonView指定mCode,那么它的行为与一般的ImageView没什么实质的
区别。在KeyButtonView中另外一个重要的字段是mSupportsLongPress,它的取值决定了
用户长按KeyButtonView时是否产生按键的长按事件。这两个字段可以在layout中通过
systemui:keyCode 以及systemui:keyRepeat 进行设置。

      导航栏中有4个KeyButtonView,分别是@id/back、@id/home、@id/recent_app以及@id/menu。
除了@id/recent_app以外,其他三个KeyButonView都被设置了相应的mCode,用于产
生KEY_BACK、KEY_HOME以及KEY_MENU三种按键事件。@id/recent_app并不会产生按
键事件,在其上的点击动作会显示或隐藏RecentPanel。

1.从触摸事件到按键事件的映射

     既然KeyButtonView的工作是将触摸事件转换为按键事件,因此其核心工作位于
onTouchEvent()方法中。
参考其代码:
[KeyButtonView.java->KeyButtonView.onTouchEvent()]
public boolean onTouchEvent(MotionEvent ev){
    final int action = ev.getAction();
    int x,y;
    switch (action){
        case MotionEvent.ACTION_DoWN:
            mDownTime=Systemclock.uptimeMillis();
            setPressed(true);
            /*①如果KeryButtcnVleuw被设置了mCode,则创建并发送给InputDispatcher 一个ACTION_DOWN
              的按健事件
*/
            if(mCode!=0){
                sendEvent(KeyEvent.ACTION_DOWN,0,mDownTime);
            }else {......}
            /*若KeyButtonview被设置为支持长按事件,则发送一个名为mCheckLongPress的
              Runnable,并在延迟一段时间后执行它。这个Runnable会重新发送一个带有LONG_PRESS标
              记的ACTION_DOWN
*/
            if(mSupportstongpress){
                removeCallbacks(mCheckLongPress);
                postDelayed(mCheckLongPress,ViewConfiguration.getLongPressTimeout());

            }
            break;
            ......
        case MotionEvent.ACTION_CANCEL:
            setpressed(false);
            if(mCode != 0){
                //②当触摸事件被取消,则发送一个带有FLAG_CANCELED标记的ACTION_UP按键事件
                sendEvent(KeyEvent.ACTION UP,KeyEvent.FLAG CANCELED);
            }
            //因为触摸事件被取消,所以不需要发送长按事件。此时需要取消尚未执行的mCheckLongPress
            if(mSupportsLongpress){
                removeCallbacks(mCheckLongPress);
            }
            break;
        case MotionEvent.ACTION_UP:
            final boolean doIt = isPressed();
            setPressed(false);
            if(mCode != 0){
                if (doIt){
                    //③发送一个ACTION_UP按键事件
                    sendEvent(KeyEvent.ACTION_UP,0);
                }else{.....}
            }else{
                if (doIt){
                    //④倘若KeyButtonview中没有设置mCode,则在此时触发OnclickListener
                    performClick();

                }
            }
            //既然用户已经抬起按在KeyButtonView上的手指,因此不再需要发送长按事件
            if(mSupportsLongpress){
                removeCallbacks(mCheckLongPress);
            }
            break;
    }
    return true;
}
简单来说,当KeyButtonView被设置了一个有效键值的情况下,将按照如下方式完成触
摸事件到按键事件的映射:
      口  触摸事件的ACTION_DOWN对应按键事件的ACTION_DOWN。
      口  触摸事件的ACTION_UP对应按键事件的ACTION_UP。
      口  触摸事件的ACTION_CANCEL对应按键事件的ACTION_UP+FLAG_CANCELED。
      口  当用户长按KeyButtonView时,对应按键事件的ACTION_DOWN+FLAG_LONGPRESSED

           
导航栏中的@id/back、@id/home 以及@id/menu 三个KeyButonView采用了上述工作方式。
另外,如果KeyButonView没有被设置一个有效的键值,那么当用户点击它时并不会产
生任何按键事件,而是触发监听器OnClickListener。导航栏中的@id/recent_app采用了这种工
作方式。

2.键盘事件的发送

      KeyButtonView.onTouchEvent()完成了从触摸事件到按键事件的映射,而KeyButtonView.
sendEvent()则完成了按键事件的创建与发送。参考其代码:
[KeyButtonView.java-->KeyButtonView.sendEvent()]
void sendEvent(int action,int flags,long when){
    //计算repeatCount
    final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS)!=0?1:0;
    //创建KeyEvent
    final KeyEvent ev=new KeyEvent(mDownrime,when,action,mCode,repeatCount,
                            0,KeycharacterMap.VIRTUAL_KEYBOARD,0,
                            flags | KeyEvent.FLAG_FROM_SYSTEN | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
                            InputDevice.SOURCE_KEYBOARD);
    /*通过InputManager.injectIngutEvent()方法将KeyEvent加入InputDispatcher的派发队列。
      INJECT_INPUT_EVENT_MODE_ASYNC表示injectInputEvent()会在事件加入派发队列后立刻返回,
      不等待事件的派发成功与否
*/
    InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}

      KeyButtonView必须自行发送长按事件并维护repeatCount,这是因为InputDispatcher仅
会为来自InputReader的事件生成长按事件以及repeatCount。

7.3.3 SearchPanel

      导航栏几乎一直显示在屏幕之上,所以它可以是用来启动某个工作的最快捷的入口,而
SearchPanel就利用了导航栏的这个优势。通过在导航栏上滑动拇指来启动搜索界面是一个既
酷又方便的操作。本节将讨论SearchPanel的启动方式以及实现原理,并且希望读者通过本节
的学习可以扩展SearchPanel以启动除了SearchPanel之外的其他功能。

1.SearchPanel的创建

      与导航栏一样,SearchPanel通过WindowManager.addView()的方式创建其窗口。
      在PhoneStatusBar.addNavigationBar()方法使用mNavigationBarView 通 过WindowManager.
addView()创建导航栏窗口之前,PhoneStatusBar 会首先调用PhoneStatusBar.prepareNavigationBar()。
而 prepareNavigationBar()会调用PhoneStatusBar.updateSearchPanel()进行 SearchPanel
的创建工作。

[PhoneStatusBar java->PhoneStatusBar.updateSearchPanel()]
protected void updateSearchPanel(){
    /*由BasestatusBar.updateSearchPanel()进行SearchPanel的创建工作。
      创建好的SearchPanel的控件树会被保存在mSearchPanelView成员中
      SearchPanel并不是小屏幕设备独有的特性,因此它的创建位于BasestatusBar*/
    super.updateSearchPanel();
    //将导航栏与SearchPanel关联起来,以便二者可以互相访问
    mSearchPanelView.setStatusBarView(mNavigationBarView);
    mNavigationBarView.setDelegateView(mSearchpanelView);
}
于是需要进入BaseStatusBar.updateSearchPenl()对SearchPanel的创建情况进行讨论。
[BaseStatusBar.java-->BaseStatusBar.updateSearchPanel()]
protected void updateSearchPanel(){
    /*①从R.layout.statua bar_search_panel中创建SearchPanel的根控件。
      保存在mSearchPanelView成员变量之中
*/
    mSearchPanelView=(SearchPanelView)LayoutInflater.from (mContext).inflate(
                           R.layout.status_bar_search_panel,tmpRoot,false/*will not add to tmpRoot*/);
    ......
    //SearchPanel默认情况下是不可见的
    msearchpanelView.setVisibility(View.GONE);
    //②创建用于SearchPanel的LayoutParams
    WindowManager.LayoutParams lp = getSearchlayoutParams(mSearchPanelView.getLayoutParams());
    /*③创建SearchPanel的窗口。不过虽然被创建,但是由于其visibility为GONE,
      因此SearchPanel仍然是不可见的
*/
    mWindowManager.addView(mSearchPanelView,lp);
    .......
}
      那么R.layout.status _bar_search_panel描述了什么样的一棵控件树呢?参考res/layout/
status_bar_search_panel.xml的内容,可以发现其根控件是一个类型为SearchPanelView的控
件,并且在嵌套几个ViewGroup之后,有一个类型为GlowPadView的控件。
SearchPanelView
以及GlowPadView是SearchPanel中最重要的两个组件。SearchPanelView作为SearchPanel控件树的根控
件为NavigationBarView以及BaseStatusBar 提供操作SearchPanel的接口
GlowPadView是用户
在启动SearchPanel之后所看到的东西。
GlowPadView在SearchPanel窗口的底部渲染了一个
半圆形的区域并在其上绘制了几个图标,当用户的手指滑动到其中某个图标之上并松手时,
GlowPadView会向SearchPanelView引发一个回调,SearchPanelView会根据图标的id执行特
定的动作。在默认情况下,GlowPadView之上仅有一个放大镜模样的图标,当用户手指滑动
其上时,SearchPanelView会启动搜索界面。
      getSearchLayoutParams()方法为SearchPanel所创建的布局参数规定了窗口的类型是
TYPE_NAVIGATION_BAR_PANEL。这个类型的窗口将拥有比导航栏更高的layer值(20),
因此SearchPanel将会显示在导航栏之上
另外SearchPanel的尺寸与导航栏一样是MATCH_
PARENT
,但是由于PhoneWindowManager并不会对TYPE_NAVIGATION_BAR_PANEL类型
的窗口做特殊的布局计算,因此SearchPanel的窗口其实是充满整个屏幕的,只不过除了
GlowPadView以外的区域都是透明的而已。

2.SearchPanel的启动

      SearchPanel的启动触发于用户在导航栏上的滑动操作,所以需要到NavigationBarView
对触摸事件的处理中寻找SearchPanel的触发方法。参考NavigationBarView.
onInterceptTouchEvent()的实现:
[NavigationBarView.java->NavigationBarView.onlnterceptTouchEvent()]
public boolean onInterceptTouchEvent (MotionEvent event){
      return mDelegateHelper.onInterceptTouchEvent(event);
}
      onInterceptTouchEvent()方法会在触摸事件被派发给子控件之前首先获得对事件进行处
理的机会,可以用来识别当前ViewGroup所感兴趣的手势。如果onInterceptTouchEvent()
法的实现者识别出手势而返回true,那么表示此ViewGroup决定独自消费这一事件序列,于
是事件序列中后续的触摸事件将会转移到当前ViewGroup.onTouchEvent()中进行处理,并且
ViewGroup的子控件将不再拥有对此事件序列的处理权。

NavigationBarView会将其截获的触摸事件发送给mDelegateHelper——一个辅助类
DelegateViewHelper的实例。而DeletegetViewHelper会通过触摸事件分析用户的手势以启动
SearchPanel。
接下来参考DelegateViewHelper的onInterceptTouchEvent()方法的实现:
[Delegate ViewHelper.java-->DelegateViewHelper.onInterceptTouchEvent()]
public boolean onInterceptTouchEvent (MotionEvent event){
    ......
    if(!mPanelShowing && event.getAction() == MotionEvent.ACTION_MOVE){
        for(int k=0;k             /*①计算用户手指滑动的距离,倘若这一距离大于mTriggermreshhold,
              则通过调用mBar.showSearchpanel()方法显示SearchPanel
*/
            if(distance>mfriggerThreshhold){
                mBar.showSearchPanel();
                mPanelshowing=true;//标记SearchPanel已经启动
                break;
            }
        }
    }
    //这部分省略的代码用于将触摸事件的坐标系从NavigationBarview转换到SearchPanelView中
    /*②将导航栏所收到的触摸事件通过dispatchrouchEvent()注入SearchpanelView中。
      这里的mDelegateView就是SearchPanelView。SearchPanelView会对触摸事件做进一步处理,

      例如将事件派发给GlowPadView绘制动画效果,以及根据用户的手势能发特定的功能*/
    mDelegateView.dispatchrouchEvent(event);
    //这部分省略的代码用于将触摸事件的坐标系从SearchPanelView转换回NavigationBarview
    /*③当SearchPanel启动后,触摸事件序列中后续的事件需要被发送给SearchPanel而不是
      NavigationBarview中的子控件。此时需要返回true,以便从子控件手中夺取事件的处理权
*/
    return mPanelshowing;
}

      DelegateViewHelper.onInterceptTouchEvent()方法中的参数event 其实是InputDispatcher
派发给导航栏窗口的触摸事件,但是它却通过View.dispatchTouchEvent()方法传递给
SearchPanelView这个位于另一个窗口的控件。正如第5章中所说,当ACTION_DOWN
被派发给一个窗口之后,其随后的事件序列都会被派发到这个窗口。
这种默认的行为使
得触摸事件可以让NavigationBarView启动SearchPanel,但是却无法被SearchPanel消
费。如此一来,用户需要在SearchPanel窗口显示后抬起手指结束当前的事件序列,再
重新按下手指使得新的事件序列被派发给SearchPanel,不过这无疑会严重影响用户的体
验。
因此DelegateViewHelper存在的意义就很明显了,它从NavigationBarView中截获事
件,并将截获的事件通过View.dispatchTouchFvent()发送给SearchPanel进行消费,使
得NavigationBarView以及SearchPanelView这两个属于不同窗口的控件树得以共享同
一个事件序列。这是操作输入事件的一个很聪明也很有用的技巧。

3.启动搜索界面

      SearchPanelView中的GlowPadView是消费触摸事件的场所。它在屏幕底部的一个半
圆形轨迹上显示一系列图标,并根据触摸事件检测用户手指的移动轨迹。当用户抬起手指
后,GlowPadView 会通过调用OnTriggerListener.onTrigger()回调将用户通过滑动手指所选
取的图标告知对此行为感兴趣的监听者。
GlowPadView上的图标是否可定制呢?答案是肯
定的。在通过XML声明GlowPadView的布局时(也就是res/layout/status_bar_search_panel.
xml
),可以通过为GlowPadView:targetDrawables属性指定一个Drawable的数组来定制出现
在GlowPadView中的图标内容与个数。默认情况下这个数组中只含有@android:drawable/
ic_action_assist_generic一个图标资源,因此用户在启动SearchPanel后只能看到一个放大镜
模样的图标。
开发者可以通过扩充这一数组的内容以定制 SearchPanel中的图标。
      SearchPanelView 提供了实现接口OnTriggerListener.on Trigger()的GlowPadTriggerListener。
因此可以在GlowPadTriggerListener.onTrigger()的实现中了解搜索界面是如何被启动的。

[SearchPanelView.java->GlowPadTriggerListener.onTrigger()]
public void onTrigger(View v,final int target){
    /*①onTrigger()中的参数target表示用户选定的图标在数组中的索引。
      不过GlowPadTriggerListenex根本不知道这一索引的含义是什么。因此,
      它通过GlowPadview.getResourceIdForTarget()方法获取此索引上的图标资源id,
      于是便可以根据不同的资源id做不同的事情*/
    final int resId = mGlowPadview.getResourceIdForTarget(target);
   switch {resId){
        case com.android.internal.R.drawable.ic_action_assist_generic:
            //②ic_action_assist_generic图标被用户选中,因此启动搜索界面
            mWaitingForlaunch=true;
            startAssistActivity();
            vibrate();//震动一下
        break;
    }
}
      同理,开发者可以在上述switch语句块中增加对新图标资源的处理。
      关于SearchPanel的内容便介绍到这里。经过本节的讨论,希望读者能够领会通过View
的事件派发方法(本例是View.dispatch TouchEvent())使多个窗口的控件共享一条事件序列的
技巧,以及如何扩展SearchPanel的功能以利用这个最快捷的功能入口。

7.3.4关于导航栏的其他话题

      除了虚拟按键以及SearchPanel之外,导航栏中还有一些值得讨论的细节问题。

1.菜单键的可见性

      一般情况下,用户在导航栏中所见到的虚拟按键只有三个:BACK键、HOME键以及
RECENT键。MENU键仅在那些提供了选项菜单的应用运行的时候才能见到。为什么呢?

      其实,用于呼出选项菜单的菜单键(无论是物理按键还是导航栏中的虚拟按键)从
Android4.0之后都不再鼓励使用。ActionBar即动作条成为莱单键的替代品,用于为用户
提供更好的用户体验。不过为了保持对旧版本的兼容性,导航栏中还是提供了对菜单键的
支持。
      当PhoneWindow为创建控件树的模板时,即PhoneWindow.generateLayout()方法中会
根据应用程序所基于的Android版本选择是否应当为此应用程序提供菜单键的支持。
参考其
代码:
[PhoneWindow.java-->PhoneWindow.generateLayout()]
protected viewGroup generateLayout(DecorView decor){
    if (targetPreHoneycomb || (targetPreIcs && targeticNeedsOptions && noActionBar)){
        /*倘若应用程序所基于的Android版本比较老,则为其窗口的LayoutParams.flags添加
          FLAG_NEEDS_MENU_KEY标记,以启用对菜单键的支持
*/
        addFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY);
    }else {
        //否则确保FLAG_NEEDS_MENU_KBY不存在于LayoutParams.flags中
        clearFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENO_KEY);
    }
}
      因此,窗口的LayoutParams.flags中是否存在FLAG_NEEDS_MENU_KEY是决定导航
栏中是否显示菜单键的根本原因
,不过这一标记是如何影响SystemUI的导航栏呢?菜单键
既然是一个按键,那么它所产生的事件一定会发送给处于焦点状态的窗口。因此获取焦点的
窗口是否存在FLAG_NEEDS_MENU_KEY便成为关键所在
当WMS完成焦点窗口的选择之
后(参考第5章),会通过PhoneWindowManager.focusChangedLw()方法将新的焦点窗口告知
PhoneWindowManager。在此方法中,PhoneWindowManager 通过updateSystemUIVisibilityLw()
方法将焦点窗口上 与SystemUI相关的设置发送给SystemUI。这些设置其中之一就是
FLAG_NEEDS_MENU_KEY 标记。
参考updateSystemUIVisibilityLw()方法的代码:
[PhoneWindowManager java-->PhoneWindowManager.updateSystemUIVisibilityLw()]
private int updatesystemuiVisibilityLw(){
    ......
    /*①检查是否需要菜单键。
      mFocusedWindow是当前的焦点窗口。而getNeedsMenuLw()对FLAG_NEEDS_MENU_KEY标记的
      检查并不仅限于焦点窗口,它还会检查位于焦点窗口之下,处于mfTopFullacreenopagqueWindowState
      (含)之上的窗口。只要这些窗口其中之一含有此标记,则表示需要在导航栏中显示菜单键
*/
    final boolean needsMenu=
                mFocusedwindow.getNeedsMenuLw (mTopFullscreenopaquewindowState);
    .....
    mHandler.post(new Runnable(){
            public void run(){
                try{
                    IStatusBarService statusbar=getStatusBarService();
                    if(statusbar != null){
                        /*②通过StatueBarManagerservice.topAppWindowChanged()方法
                          将  是否需要菜单键    通知  给SystemUI里的导航栏
*/
                        statusbar.topAppWindowChanged(needsMenu);
                    }
                }catch (RemoteException e){.…}
        });
}
既然设置菜单键可见性使用了熟悉的StatusBarManagerService,那么最终处理这一请求的
一定是BaseSatusBar 或其子类。参考PhoneStatusBar.topAppWindowChanged()的实现:
[PhoneStatusBar.java-->PhoneStatusBar.topAppWindowChanged()]
public void topAppwindowChanged (boolean showMenu)(
    //如果导航栏存在,则通过setMenuvisibility()设置某单键的可见性
    if (mNavigationBarView!=null){
        mNavigationBarView.setMenuVisibility(showMenu);
    }
    ......
}
至于NavigationBarView.setMenuVisibility()的实现,请参考:
[NavigationBar View,.java->NavigatoinBarView.setMenuVisibility()]
public void setMenuVisibility(final boolean show,final boolean force){
    if(!force && mShowMenu == show)return;
    mShowMenu = show;
    //设置菜单键的可见性
    getMenuButton().setVisibility(mShowMenu ? View.VISIBLE:View.INVISIBLE);

}
      综上所述,是否为一个窗口显示菜单键取决于PhoneWindow 中generateLayout)方法
是否为窗口添加FLAG_NEEDS_MENU_KEY,它仅对基于Android4.0之前版本的应用程
序添加此标记。当PhoneWindowManager发现焦点窗口发生变化之后,倘若从焦点窗口到
第一个全屏不透明窗口(mTopFullscreenOpaqueWindowState)之间的所有窗口中,至少有
一个窗口指定了FLAG_NEEDS_MENU_KEY,则会决定显示菜单键,否则隐藏菜单键。

PhoneWindowManager 会通过StatusBarManagerService.topAppWindowChanged()方法对导航栏
中菜单键的可见性进行设置。最终NavigationBarView通过View.setVisibility)方法将菜单键所
对应的KeyButonView进行显示或隐藏操作。

2.修改BACK键的图标

      细心的读者应该能够发现,正常情况下导航栏上的BACK键是一个向左的箭头。不过
一旦弹出了输入法窗口,BACK键则变为了一个向下的箭头,用以指示当用户按下BACK
键后将退出输入法而不是当前应用程序。其实这个行为就导航栏而言,它仅仅修改了
KeyButtonView所显示的图标而已。

      BACK键变为向下的箭头主要是为了引发用户对输入法窗口退出时所使用的向下滑出
屏幕动画的联想,因此不要尝试修改输入法退出时的这种动画形式。

      当一个文本框使用InputMethodManager.showSoftInput()尝试弹出输入法窗口时,这一请
求经由InputMethodManagerService 转发给实现输入法的InputMethodService
,并由后者着手创
建并显示输入法的窗口。
[InputMethodService.java->InputMethodlmpl.showSoftInput()]
public void showSoftInput(int flags,ResultReceiver resultReceiver){
    ........ //显示输入法窗口的代码
    boolean showing=onEvaluateInputViewShown();
    /*将输入法窗口的显示状态通知给InputMethodlManagerService。
      注意,当窗口处于显示状态时,此方法的第二个参数包含IME_VISIBLE标记
*/
    mImm.setImeWindowStatus(mToken,IME_ACTIVE I(showing ? IME_VISIBLE:0),mBackDisposition);
    .......
}
      随后,由setlmeWindowStatus()所设置的输入法窗口状态辗转经过
InputMethodManagerService、StatusBarManagerService来到PhoneStatusBar:

[PhoneStatusBar.java-->PhoneStatusBar.setImeWindowStatus()]
public void setImewindowstatus(TBinder token,int vis,int backDisposition){
    //altBack决定是否修改Back键的图标。其中一个充分条件就是第二个参数中存在IME_VISIBLE标记
    boolean altBack=
        (backDisposition==InputMethodService.BACK_DISPOSITION_WILL_DISMISS)
        ||((vis & InputMethodService.IME_ VISTBLE)!=0);
    /*通过mCommandQueue.setNavigationIconHints()方法修改BACK键的图标。
      注意,若altBack为true,即需要修改BACK键的图标时,
      NAVIGATION_HINT_BACK_ALT标记存在于参数之中
*/
    mCommandoueue.setNavigationIconHints(
                altBack?
                (mNavigationTconHintsl StatusBarManager.NAVIGATION HINT BACK ALT)
                :
                (mlavigationIconHints&~StatusBarManager.NAVIGATION HINT_BACK_ALT));
    ......
}
      NavigationBarView.setNavigationlconHints()方法用于对导航栏上的虚拟按键的图标进行微
调。其参数hint可以是下列标记的组合:

      口  NAVIGATION_HINT_BACK_NOP,表示此时的BACK键无意义。参数中含有此标记
            时,NavigationBarView将通过调整BACK键的透明度使其可见性降低。
      口  NAVIGATION_HINT_HOME_NOP,表示此时的HOME键无意义。参数中含有此标
            记时,NavigationBarView将通过调整HOME键的透明度使其可见性降低。
      口  NAVIGATION_HINT_RECENT_NOP,表示此时的HOME键无意义。参数中含有此
            标记时,NavigationBarView将通过调整RECENT键的透明度使其可见性降低。
      口  NAVIGATION_HINT_BACK_ALT,修改BACK键的图标。

那么这些标记是如何生效的呢?请参考NavigationBarView.setNavigationlconHints()的
实现:
[NavigationBarView.java-->NavigationBarView.setNavigationlconHints()]
public void setNavigationIconHints (int hints,boolean force){
    .......
    //保存hints
    mNavigationIconHints-hints;
    //根据三个_NOP标记设置三个虚拟按键的透明度
    getBackButton(.setAlpha(
        (0!=(hintss StatusBarManager.NAVIGATION HINT BACK NOP))?0.5f:1.0f);
    getHomeButton().setAlpha(
        (0!=(hints & StatusBarManager.NAVIGATION HINT HOME NOP))? 0.5f:1.Of);
    getRecentsButton().setalpha(
        (0!=(hints6 StatusBarManager.NAVIGATION_HINT_RECENT_NOP))?0.5f:1.0f);
    /*若hints参数中存在NAVIGATTON_HINT_BACK_ALT,则使用mBackAltTcon或mBackAltLandIcon
      作为BACK键的图标(下箭头),否则使用mBackIcon或mBackLandIcon(左箭头)
*/
    ((ImageView)getBackButton()).setImageDrawable(
        (0!=(hints& StatusBarManager.NAVIGATION_HINT_BACK_AT))
            ?(mVertical?mBackAltLandIcon:mBackAltIcon)
            :(mVertical?mBackLandIcon:mBackIcon));
    ......
}
可见,这些标记并不会实质性地改变虚拟按键的行为,仅调整了它们的显示方式而已。
      另外,目前并没有哪个系统组件需要对导航栏设置NOP结尾的三个标记。因为当某
个虚拟按键没有意义时(例如当不响应BACK键的应用程序处于运行状态时),最好的办法
是将其完全隐藏,而不是仅仅让其可见性降低。这就需要用到SystemUI的另外一个机制
DisableActions。我们将在7.4节介绍。

3.导航栏方向的选择

      如前面所述,导航栏的根控件NavigationBarView之下有两套不同方向的控件树@id/rot0
和@id/rot90。当导航栏处于屏幕底端时的布局为@id/roto,而处于屏幕右端时的布局
为@id/rot90。
导航栏是如何在这两者之间进行切换的呢?常规的思路是通过监听ACTION_
CONFIGURATION_ CHANGED广播,并根据广播到来时所携带的屏幕方向信息对这两棵控件
树进行选择。不过,鉴于PhoneWindowManager对导航栏的特殊布局计算方式,导航栏选择
了一个更聪明的做法。
      每当屏幕方向发生旋转时,WindowManagerService会对所有窗口进行重新布局,进而使
得PhoneWindowManager.beginLayoutLw()被调用。
在这个方法中,PhoneWindowManager根
据屏幕在当前方向下的宽高信息选择将导航栏的窗口置于屏幕底端或者右端
导航栏窗口的
位置发生变化时
,其尺寸同样会发生变化(底端时为宽大于高,而右端是为高大于宽)。
导航栏窗口尺寸的变化会引发ViewRootlmpl的“遍历”操作,进而使得导航栏的控件树进
行重新布局。而控件树重新布局的结果是NavigationBarView控件的尺寸发生变化,于是
NavigationBarView 重写了View.onSizeChanged()方法并在这里完成@id/rot0与@id/rot90的选
择。
参考其代码:
[NavigationBarView.java->NavigationBarView.onSizeChanged()]
protected void onsizechanged(int w,int h,int oldw,int oldh){
    //检查NavigationBarView处干竖值状态还是水平状态
    final boolean newVertical=w>0 && h>w;
    //当竖直或水平状态发生变化时
    if (newVertical != mVertical){
        mVertical = newvertical;
        //reorient()会在eid/rot0、eid/rot90与0id/rot90之间做出选择
        reorient();
    }
    super.onSizeChanged(w,h,oldw,oldhl;
}
再看reorient()方法的实现:
[NavigationBar View.java->NavigationBarView.reorient()]
public void reorient(){
    //首先获取屏幕的旋转角度:0、90、180或270
    final int rot=mDisplay.getRotation();
    /*mRotatedViews数组存储了用于特定方向的导航栏控件树。进行控件树的选择之前,
      首先将所有控件树的Visibility设置为GONE
*/
    for (int i=0;i<4;i++){
        mRotatedViews[i].setVisibility(View.GoNE);
    }
    //①选择对应当前旋转方向的控件树作为mCurrentView,并将其设置为可见
    mCurrentView=mRotatedViews[rot];
    mcurrentView.setVisibility(View.VISTBLE);
    //更新mDeadzone使之引用当前控件树中的Deadzone
    mDeadDone=(Deadzone)mCurrentView.findViewById(R.id.deadzone):
    //②为当前控件树同步各种状态
    //低辨识度状态
    setLowProfile(mLowProfile,false,true/*force*/);
    //禁用功能
    setDisabledFlags(mDisabledFlags,true /* force */);
    //菜单键的可见性
    setMenuVisibility (mshowMenu,true /*force*/);
    //修改虚拟按键的图标
    setNavigationIconHints(mNavigationIconHints,true);
}
      其中,mRotatedViews数组的初始化位于NavigationBarView.onFinishlnflate()方法。此
方法在Layoutlnflater完成从layout中创建控件树之后被调用,因此初始化此数组的时机在
PhoneStatusBar.makeStatusBar()方法
。参考其实现:
[NavigationBar View.java->NavigationBarView.onFinishlnflate()]
public void onFinishInflate(){
    //0度和180度(竖直状态)使用@id/roto
    mRotatedViews[Surface.ROTATION_0]=
            mRotatedviews[Surface.ROTATION_180] = findViewById(R.id.rot0);
    //90度(水平状态)使用@id/rot90
    mRotatedviews[Surface.RorArION_90] = findViewById(R.id.rot90);
    //NAVBAR_ALWAYS_AT_RIGHT目前是true,因此270度的水平状态下仍然使用Qid/rot90
    mRotatedViews[Surface.ROTATION_270] = NAVBAR ALWAYS AT_RIGHT
                                    ?findViewById(R.id.rot90)
                                    :findviewById(R.id.rot270);
    mCurrentView=mRotatedViews [Surface.ROTATION_0];
}
      简单来说,导航栏选择不同控件树的时机在onSizeChanged()方法中,然后reorient()通
过mDisplay.getRotation()获取屏幕方向,进而在mRotatedViews数组中选择@id/rot0和@id/
rot90两棵控件树之一进行显示。

7.3.5导航栏总结

对导航栏的介绍至此告一段落。总结一下本节的重点内容:
口  导航栏的核心工作是将用户的触摸事件转换为相应的按键事件,并发送给
      InputDispatcher。而这一功能的主要实现者是导航栏内的KeyButonView。
口  PhoneWindowManager会使用特殊的形式对导航栏的窗口进行布局。屏幕处于竖直方
      向时的导航栏会被布局在屏幕的底端,而水平方向的导航栏会被布局在屏幕的右端。
      在这两种布局下,导航栏会分别选择@id/rot0以及@id/rot90作为其控件树进行显示。
口  导航栏还提供了setMenuVisibility()以及 setNavigationlconHints(),以允许PhoneStatusBar
      对导航栏的行文进行微调。
另外,导航栏还支持进入低辨识度模式,以及可以禁用一些功能。这些内容涉及
SystemUIVisibility以及DisableAction两个话题,它们将在下一节讨论。

7.4禁用状态栏与导航栏的功能

      在某些情况下,Android需要禁用状态栏或导航栏中的某些功能。例如在锁屏状态下,用
户点击HOME键、BACK键以及RECENT键是无意义的,因此最好将这些虚拟按键隐藏掉。
同样在锁屏状态下,尤其是用户通过图案或密码进行解锁保护的时候,Android不希望用户可
以拉出下拉卷帘而导致那些如短信等私人信息遭到暴露。

7.4.1如何禁用状态栏与导航栏的功能

      为此StatusBarManager 提供了disable()方法使得开发者可以禁用状态栏或导航栏的一些
功能。

[StatusBarManager java->StatusBarManager.disable()]
public void disable(int what){
    try{
        final IStatusBarService svc = getService();
        if(svc!=null){
            //向StatusBarManagerService发送禁用某些功能的请求
            svc.disable(what,mToken,mContext.getPackageName());

        }
    }catch {Remotesxception ex){......}
}
disable()方法的参数what可以是如下禁用标记中的一个或多个的组合:
口  DISABLE_EXPAND,禁止用户拉出下拉卷帘。
口  DISABLE_NOTIFICATION_ICONS,隐藏状态栏中的通知图标。
口  DISABLE_NOTIFICATION_ALERTS,禁用通知声音。
口  DISABLE_NOTIFICATION_TICKER,禁用Ticker。
口  DISABLE_SYSTEM_INFO,隐藏状态栏中系统状态图标区、信号信息、电量信息以
      及系统时钟。
口  DISABLE_HOME,隐藏 HOME键。
口  DISABLE_RECENT,隐藏 RECENT键。
口  DISABLE_BACK,隐藏BACK键。
口  DISABLE_SEARCH,禁止启动SearchPanel。
口  DISABLE_CLOCK,隐藏时钟。

      另外值得一说的是mToken参数。mToken是随着StatusBarManager一起被创建的一个
Binder实例,
可以理解为它是StatusBarManager的一个可以跨进程传递的ID。那么它存在
的意义是什么呢?以一个合理的逻辑来看,既然禁用某一个功能,那么一定要在之后的某
一个时机恢复这一功能。倘若一个应用程序通过StatusBarManager.disable()禁用了某一个功
能之后因为某种原因意外退出,那么这一被禁用的功能有可能永远无法恢复。为了避免这
个问题,StatusBarManagerService 要求StatusBarManager必须提供一个Binder实例,以便
StatusBarManager 所在的进程意外退出之后StatusBarManagerService可以通过DeathReceipient
机制得到通知,并恢复这一进程所禁用的功能。

      禁用状态栏或导航栏的功能需要StatusBarManager.disable()方法的调用者拥有签名级
的系统权限android.permission.STATUS_BAR

7.4.2 StatusBarManagerService对禁用标记的维护

      在StatusBarManagerService中,接受StatusBarManager.disable()请求的方法是
StatusBarManagerService.disableLocked()。
[StatusBarManagerService.java-->StatusBarManagerService.disableLocked()]
private void disableLockedlint userId,int what,IBinder token,String pkg){
    /*①在manageDisableListtocked()里,userId、what、token、pkg等信息会被封装为一个
      DisableRecord实例,然后保存在mDisableRecords列表中
*/
    manageDisableListLocked(userId,what,token,pkg);
    /*②gatherDisableActionsLocked()会在mDiaableRecords中造历所有DisableRecord实例,
      然后收集它们所保存的禁用标记并作为返回值返回给net
*/
    final int net gatherDisableActionsLocked(userId);
    /*③最后把收集到的禁用标记发送给NotificationManagerService
      以及SystemUI中的状态栏与导航栏
*/
    if (net != mDisabled){
        mDisabled=net;
        /*把禁用标记设置给NotificationManagerService。NotificationManagerService所关心的禁
          用标记仅有DISAELE_NOTIETCATION_ALERT一个,用来在添加通知之时跳过通知音的播放或震动
*/
        mHandler.post(new Runnable(){
                public void run(){
                    mNotificationCallbacks.onSetDisabled(net);
                }
            });
        //把禁用标记通过mBar设置给SystemUI中的状态栏与导航栏
        if(mBar != null){
            try{
                mBar.disable(net);
            }catch(RemoteException ex){}
        }
    }
}
      作为一个系统服务,StatusBarManagerService 可以通过StatusBarManager为多个进程提供
disable()方法的调用。为了避免不同进程所设置的禁用标记发生冲突(如进程A所设置的禁
用标记把进程B所设置的禁用标记替换掉),StatusBarManagerService为每一个需要禁用功能
的进程维护了一个DisableRecord并保存在mDisableRecords中。于是StatusBarManagerService
可以通过遍历所有的DisableRecord来收集每个进程所提供的禁用标记,并以收集的结果设置
给NotificationManagearService以及SystemUl中的状态栏与导航栏,以保证满足每个进程对禁
用功能的需求。

7.4.3状态栏与导航栏对禁用标记的响应

      BaseStatusBar并没有提供disable()方法的实现,这是由子类完成的。以PhoneStatusBar
的实现为例,状态栏与导航栏对禁用标记的响应方式主要是设置特定控件的可见性。
PhoneStatusBar.disable()的实现十分清晰简单,这里直接列出此方法对不同禁用标记的响应行
,如下所示:
      口  DISABLE_SYSTEM_INFO,使mSystemlconArea以动画的方式将透明度设置为0,使
            之变得不可见。mSystemlconArea是系统状态图标区、信号信息、电量信息以及系统
            时钟的父控件。
      口  DISABLE_CLOCK,设置控件@id/clock的可见性为GONE。@id/clock是维护系统时
            钟的控件Clock。
      口  DISABLE_EXPAND,立刻收起下拉卷帘,并且禁止PhoneStatusBarView.onTouch-
            Event()对用户手势的识别。
      口  DISABLE_HOME、DISABLE_BACK、DISABLE_RECENT 由 NavigationBarView
            进行处理。NavigationBarView会将对应的KeyButtonView的可见性设置为INVISIBLE。
      口  DISABLE_SEARCH  由NavigationBarView进行处理。DISABLE_SEARCH禁用标记的
            存在会在DelegateViewHelper.onInterceptTouchEvent()方法中阻止DelegateViewHelper
            对触摸事件进行处理。进而使得SearchPanel因为无法启动而被禁用。尤其是,倘
            若DISABLE_HOME标记存在而DISABLE_SEARCH标记不存在,则NavigationBarView
            中的@id/search_light控件将会显示出来,以提示用户虽然HOME键不可用,
            但仍然可以通过在导航栏上滑动手指启动SearchPanel。
      口  DISABLE_NOTIFICATION_ICONS,使mNotificationlcons以动画的方式将透明度设
            置为0,使之变得不可见。mNotificationIcons是容纳通知图标的容器。
      口  DISABLE_NOTIFICATION_TICKER,立刻终止正在进行的ticking动作,并在Phone
            StatusBar.tick()方法中阻止tick。这样一来通知的内容将不会出现在状态栏之上。
      口  DISABLE_NOTIFICATION_ALERT,这个禁用标记并没有在PhoneStatusBar中进行处
            理。对这个禁用标记感兴趣的是NotificationManagerService,用来在添加通知之时跳
            过通知音的播放与震动。

      其实,查看这些禁用标记在StatusBarManager中的定义,可以发现它们一一对应于
定义在View类中的以STATUS_BAR_为前缀的标记。这些STATUS_BAR_标记属于另外
一个话题—SystemUIVisibility。或者说,除了通过StatusBarManager.disable()方法之外,
SystemUIVisibility是另外一种禁用状态栏与导航栏功能的方式。

7.5理解 SystemUIVisibility

      尽管StatusBarManager 以及StatusBarManagerService为应用程序以及系统服务提供了操
作状态栏与导航栏的所有接口,但是这些接口并不适用于那些没有系统签名的普通应用程序。
倘若一个普通的应用程序希望对状态栏以及导航栏进行操作,就必须使用SystemUlVisibility
机制。

      SystemUIVisibility与禁用标记一样,是一个int型的变量,其中可以按位容纳多个定义在
View类中用于调整状态栏与导航栏行为的标记。
其实SystemUIVisibility可容纳的标记的丰富
性远远不止控制SystemUI的可见性而已。按照这些标记所产生的影响,它们可以分为以下三类。
影响状态栏与导航栏可见性的标记:
      口  SYSTEMUI_FLAG_LOW_PROFILE,使得状态栏与导航栏进入低辨识度模式。
      口  SYSTEM_UI_FLAG_HIDE_NAVIGATION,隐藏导航栏。窗口在布局时的ParentFrame、
            ContentFrame 以及DisplayFrame都会延展到整个屏幕。
      口  SYSTEM_UI_FLAG_FULLSCREEN,隐藏状态栏。窗口在布局时的ParentFrame、
            ContentFrame以及DisplayFrame都会延展到屏幕的顶部。

SYSMTE_UI_FLAG_FULLSCREEN 并不是同时隐藏状态栏与导航栏。

影响窗口布局结果的标记:
      口  SYSTEM_UI_FLAG_LAYOUT_STABLE,声明这个标记的窗口的ContentFrame不会
            随着导航栏或状态栏的显示或隐藏而发生变化。
      口  SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,PhoneWindowManager在对声
            明了这个标记的窗口进行布局时会认为导航栏已经隐藏,无论导航栏是否真的被隐藏。
      口  SYSTEM_UI FLAG_LAYOUT_FULLSCREEN,PhoneWindowManager在对声明了这
            个标记的窗口进行布局时会认为导航栏和状态栏都已经隐藏,无论状态栏和导航栏是
            否真的被隐藏。

用于禁用状态栏与导航栏的某些功能的标记:
      口  STATUS_BAR_DISABLE_EXPAND
      口  STATUS_BAR_DISABLE_NOTIFICATION_ICONS
      口  STATUS_BAR_DISABLE_NOTIFICATION_ALERTS
      口  STATUS_BAR_DISABLE_NOTIFICATION_TICKER
      口  STATUS_BAR_DISABLE_SYSTEM_INFO
      口  STATUS_BAR_DISABLE_HOME
      口  STATUS_BAR_DISABLE_BACK
      口  STATUS_BAR_DISABLE_CLOCK
      口  STATUS_BAR_DISABLE_RECENT
      口  STATUS_BAR_DISABLE_SEARCH

      这些以STATUS_BAR_DISABLE_为前缓的标记的功能与在StatusBarManager中所定
义的禁用标记一一对应并且功能相同,并且可以通过systemUIVisibility &StatusBar-
Manager.DISABLE_MASK这种方式将它们从SystemUIVisibility中提取出来,然后作
为StatusBarManager.disable()方法的参数设置给状态栏与导航栏。不过目前Android
中并没有做出这种行为的代码。因此,通过SystemUIVisibility的方式进行状态栏与导
航栏功能的禁用目前是行不通的。

      设置SystemUIVisibility的方式有两种一是在任意一个已经显示在窗口上的控件调用
View.setSystemUiVisibility(),
二是直接在窗口的LayoutParams.systemUiVisibility上进行设置并
通过WindowManager.updateViewLayout()方法使其生效。

7.5.1SystemUIVisibility 在系统中的漫游过程

      从可用的标记列表可以看出SystemUIVisibility主要涉及状态栏和导航栏的行为 以及
窗口布局两个方面,因此SystemUIVisibility的消费者有SystemUI中的BaseStatusBar,
及负责窗口布局的PhonewindowManager。本节将View.setSystemUIVisibility()开始追寻
SystemUIVisibility从控件到BaseStatusBar 以及PhoneWindowManager的过程。

1.控件树中的SystemUIVisibility

参考View.setSystemUiVisibility()的实现:
[View.java->View.setSystemUIVisibility()]
public void setSystemUIvisibility(int visibility){
    if (visibility != mSystemUiVisibility){
        //①首先,SystemUIvisibility会保存在View自己的成员变量mSystemUIvieibility中
        mSystemUiVisibility=visibility;
        //②通知父控件这一变化
        if(mParent!=null && mAttachInfo!=null
                &&!mAttachInfo.mRecomputeGlobalAttributes){
            mParent.recomputeViewAttributes(this);
        }
    }
}
recomputeViewAtributes()会沿着控件树向根部回溯,最终ViewRootlmpl
recomputeViewAttributes()
会对此做出如下处理:
[ViewRootlmpl.java->ViewRootlmpl.recomputeViewAttributes()]
public void recomputeViewAttributes(View child){
    checkThread();//必须在窗口的主线程中调用
    if (mView == child){
        //①标记随后需要处理SystemUIVisibility的变化
        mAttachInfo.mRecomguteGlobalAttributes = true;
        if(!mWillDrawsoon){
            //②触发一次“遍历”动作
            scheduleTraversals();
        }
    }
}
      在ViewRootlmpl的“遍历”过程中会执行ViewRootlmpl.collectViewAttributes()方法收集
控件树中每个View所保存的SystemUIVisibility
。其实现如下:
[ViewRootlmpl.java-->ViewRootImpl.collectViewAttributes()]
private boolean collectViewAttributes(){
    final View.AttachInfo attachInfo = mAttachInfo;
    if (attachInfo.mRecomputeGlobalAttributes){
        ......
        //①清空AttachInfo中所保存的SystemUIVisibility
        attachInfo.mSystemUIVisibility = 0;
        /*②View.dispatchCollectViewAttributes()会遍历整个控件树。
          并在这个过程中以按位或的方式将每个控件所存储的SystemUIVisibility累加到
          attachInfo.mSystemUIVisibility之上
*/
        mView.dispatchCollectViewAttributes(attachInfo,0);
        //从收集到的SystemUIVisibility中移除被禁用的SystemUIVisibility标记
        attachInfo.mSystemUiVisibility &=~attachInfo.mDisabledsystemUIVisibility;
        WindowManager.LayoutParame params = mWindowAttributes;
        if(...... || attachInfo.mSystemuiVisibility != params.subtreesystemuiVisibility){
            //③将SystemUIVisibility保存到窗口的Layoutparams中
            params.subtreeSystemUIVisibility=attachInfo.mSystemUIVisibility;
            ......
            return true;
        }
    }
    return false;
}
      可见,在控件树中,每一个控件都在View.mSystemUiVisibility中保存了本控件所
期望的SystemUIVisibility。当ViewRootlmpl进行“遍历”操作时,会通过View.dispatch-
CollectViewAtributes()方法将每个控件所期望的SystcmUTVisibility收集到Attachlnfo.
mSystemUiVisibility成员中,并保存到当前窗口的LayoutParams.sutreeSystemUiVisibility作为
本窗口所期望的SystemUIVisibility。当ViewRootlmpl通过WMS.relayoutWindow()方法对
窗口进行重新布局时,本窗口所期望的SystemUIVisibility 会伴随着LayoutParams.subtree-
SystemUiVisibility以及LayoutParams.systemUiVisibility两个字段进入WMS。

      在View.dispatchCollectViewAtributes()收集所有控件所期望的SystemUIVisibility之后,
会将Attachlnfo.mDisabledSystemUiVisibility中所定义的标记从收集结果中移除。这是
因为ActionBar(动作条)的存在所导致的。Android的UE行为规定,当ActionBar以
Overlay的形式显示的时候,状态栏必须也处于显示状态。于是,在ActionBar显示时,
会通过View.setDisabledSystemUiVisibility()将 SYSTEM_UL_FLAG_FULLSCREEN
作为禁用的标记并保存在Attachlnfo.mDisabledSystemUiVisibility中。进而在这里将
SYSTEM_UI_FLAG_FULLSCREEN从收集到的SystemUiVisibility中移除。在Phone-
WindowManager中也有类似的操作。

2.WMS中的SystemUIVisibility

      既然一个窗口所期望的SystemUIVisibility会通过WMS.relayoutWindow()方法进入
WMS,于是可以参考WMS.relayoutWindow()中与SystemUIVsibility相关的代码:
[WindowManagerService.java->WindowManagerService.relayoutWindow()]
public int relayoutwindow (Session session,IWindow client,int seq,
                        WindowManager.LayoutParams attrs,......){
    ......
    int systemUiVisibility = 0;
    if (attrs != null){
        /*①LayoutParama中的systemuiViaibility以及从控件树中收集到的
          subtreeSyetemUiVisibility会被整合在一起
*/
        systemuiVisibility =
                    (attrs.systemuivisibility | attrs.subtreesystemuiViaibility);
        /*因为SystemUIVisibility中以STATUS_BAR_DISABLE_为前级的标记其实是7.4节所介绍的禁用
          标记,因此必须确保调用者拥有相应的权限。否则这些禁用标记会被移除
*/
        if((systemuiVisibility & StatusBarManager.DISABLE_MASK)!=0){
            if(mContext.checkCallingOrSelfPermission(
                    android.Manifest.permission.STATUS_BAR)
                    !=PackageManager.PERMISSION_GRANTED){
                systemUiVisibility s=~StatusBarManager.DISABLE_MASK;
            }
        }
    }
    ......
    synchronized(mWindowMap){
        //获取执行relayout操作的窗口的windowState
        windowstate win = windowForClientLocked (session,client,false);
        ......
        //②将窗口所期望的SystemurVisibility保存到Windowstate.mSystemuiVisibility
        if (attrs != null && seq==win.mSeq)(
            win.mSystemUiVisibility = systemUiVisibility;
        }
        ......
    }
    ......
}
      所以在WMS中,每个窗口的WindowState在其成员变量mSystemUiVisibility中保存了各
自期望的SystemUIVisibility。SystemUIVisibility消费者之一的PhoneWindowManager可以通
WindowState.getSystemUiVisibility()获取这一信息并据此对窗口进行布局,或设置状态栏与
导航栏的可见性。

      不过SystemUiVisibility在系统中漫游的脚步仍然没有停止,因为除了PhoneWindowManager之外,
SystemUI进程中的BaseStatusBar及其子类也是SystemUIVisibility的消费者。
不过
在WMS中并没有像ViewRootimpl一样将所有窗口所期望的SystemUIVisibility收集在一
起,焦点窗口的SystemUIVisibility才能传递给SystemUI中的BaseStatusBar。   

当焦点窗口发生变化时所调用的PhoneWindowManager.focusChangedLw()(请
参考第5章与窗口焦点相关的内容),以及WMS在布局过程所调用的PhoneWindowManager.
finishPostLayoutLw()
(请参考第4章与窗口布局相关的内容)两个方法中会调用
PhoneWindowManager.updateSystemUiVisibilityLw()方法,将焦点窗口的SystemUIVisibility
传递给BaseStatusBar。另外,设置导航栏中菜单键可见性的也是这个方法。
参考其实现:
[PhoneWindowManager.java-->PhoneWindowManager.updateSystemUiVisibilityLw()]
private int updatesystemuiVisibilityLw(){
    /*①从焦点窗口中获取它所期望的SystemUiVisibility。
      Phonewindowyanager删除mResettingSystemUiFlags以及mForceclearedSystemUiFlags中
      所有的标记。其目的类似于AttachInfo.mDisabledsystemuiVisibility。
      有时候PhoneWindowManager不希望存在某些标记,这部分内容将在介绍
      SYSTEM_UI_FLAG_HIDE_NAVIGATION的处理时进行介绍
*/
    final int visibility = mFocusedwindow.getSystemUiVisibility()
                                &~mResettingsystemUiFlags
                                &~mForceClearedSystemUiFlags;
    //②过滤后的SystemUiVisibility被保存到PhoneWindowManager.mLastSystemUiFlags
    mLastSystemuiFlags = visibility;
    ......
    mHandler.post(new Runnable(){
        public void run(){
            try{
                IStatusBarService statusbar=getStatusBarService();
                if(statusbar != null){
                    //③将SystemUIVisibility设置给systemUI进程中的BasestatusBar
                    statusbar.setSystemUiVisibility(visibility,OXffffffff);
                }
            }catch (RemoteException e){......}
        });
    return diff;
}
      可见在PhoneWindowManager中,焦点窗口所期望的SystemUIVisibility会被保存到
PhoneWindowManager.mSystemUiVisibility中。

3.状态栏与导航栏中的SystemUIVisibility

      BaseStatusBar 并没有提供 setSystemUiVisibility的实现,因此仍然以PhoneStatusBar的实
现为例进行探讨。
[PhoneStatusBar.java->PhoneStatusBar.setSystemUiVisibility()]
public void setSystemUiVisibility(int vis,int mask){
    .......
    if(diff != 0){
        //①保存SystemUiVisibility到mSyatemUiVisibility成员变量之中
        mSystemUiVisibility=newVal;
        //②PhonestatueBar只负责处理SYSTEM_UI_PLAG_LOW_PROPTLE标记
        if {0!={diff & View.SYSTEM_ UI_FLAG_LOW_PROFILE)){
            final boolean lightsOut=(0!=(vis & View.SYSTEM_UI_FLAG_LOW_PROFILE));
            ......
            if(mNavigationBarView!=null){
                //使导航栏进入或推出低辨识度模式
                mNavigationBarView.setLowProfile(lightsOut);

            }
            //使状态栏进入低辨识度模式
            setStatusBarLowprofile(lightsOut
);
        }
        //③将mSyetemuiViaibility设置回WMS
        notifyUiVisibilitychanged();
    }
}
      PhoneStatusBar会将PhoneWindowManager所给予的SystemUIVisibility保存到PhoneStatusBar.
mSystemUiVisibility。另外可以看出PhoneStatusBar 只对SYSTEM_UIFLAG_LOW_PROFILE
标记进行处理。随后,notifyUiVisibilityChanged()的调用将会把mSystemUiVisibility设置到WMS。

4.回到WMS的SystemUiVisibility

      WindowManagerService中的statusBarVisibilityChanged()方法将会接受来自
PhoneStatusBar的SystemUiVisibility。

[WindowManagerService java->WindowManagerService.statusBarVisibilityChanged()]
public void statusBarVisibilitychanged(int visibility){
    ......
    synchronized(mWindowMap){
        //①保存到mLastStatusBarVisibility成员中
        mLastStatusBarViaibility = visibility;
        /*②过滤来自SystemUI的SystemUIVisibility
          phonewindowManager.adjustsystemuivisibility()会移除mResettingsSystemUi-
          Flags以及mForceClearedsystemUiFlags中所定义的SystemUIVisibility标记
*/
        visibility = mPolicy.adjustSystemiVisibilityLw(visibility);
        //③把修正后的SystemUIVisibility通知给每一个窗口,并发送给窗口所在进程的ViewRootlmpl
        updateStatusBarVisibilityLocked(visibility);
    }
}
      WMS 将SystemUIVisibility 保存到mLastStatusBarVisibility中。
      最后updateStatusBarVisibility()方法将会被PhoneWindowManager修正过的SystemUI-
Visibility存储到每一个WindowState中,并发送给窗口所在进程的ViewRootlmpl。如前文所
属,PhoneWindowManager对SystemUIVisibility的修正在于移除mResettingSystemUiFlags以及
mForceClearedSystemUiFlags所定义的标记。

5.回到ViewRootimpl的 SystemUiVisibility

      修正过的SystemUiVisibility会通过IWindow.dispatchSystemUiVisibilityChanged()方法通
知到窗口所在进程的ViewRootlmpl。ViewRootlmpl会通过View.updateLocalSystemUiVisibility()
将遍历控件树中的每一个控件以更新控件中所保存的View.mSystemUiVisibility。同时注册在
控件中的OnSystemUiVisibilityChangeListener监听器会通知对SystemUiVisibility的变化感兴
趣的组件。

6.总结

      可见,SystemUiVisibility从一个控件的 setSystemUiVisibility()开始,先后经历了ViewRootlmpl、
WMS、PhoneWindowManager、PhoneStatusBar、WMS、ViewRootlmpl几个组件的
处理与保存,最后回到控件中。在这个过程中如下几个位置保存了SystemUIVisibility以供
使用:

      口  View.mSystemUiVisibility,保存了一个控件所期望的SystemUIVisibility。
      口  Atachlnfo.mSystemUiVisibility,保存了整个控件树所期望的SystemUiVisibility,它是
            控件树中每个控件所期望的SystemUiVisibility之和。
      口  LayoutParams.systemUiVisibilityLayoutParams.subtreeSystemUiVisibility,二者一起
            构成窗口所期望的SystemUiVisibility。其中LayoutParams.subtreeSystemUiVisibility等
            同于AttachInfo.mSystemUiVisibility。
      口  WindowState.mSystemUiVisibility,在WMS中保存了一个窗口所期望的SystemUiVisibility,
            将被PhoneWindowManager用来调整对此窗口的布局行为。尤其是,当此窗口
            是焦点窗口时,它将会影响状态栏与导航栏的可见性。
      口  PhoneWindowManager.mLastSystemUiFlags,在PhoneWindowManager中保存焦点窗口
            所期望的SystemUiVisibility,将用来影响状态栏与导航栏的可见性。
      口  PhoneStatusBarmSystemUiVisibility,其值等同于PhoneWindowManager.mLastSystemUiFlags。
           目前PhoneStatusBar 会根据其中的 SYSTEM_UL_FLAG_LOW_PROFILE标记将状态
            栏与导航栏设置为低辨识度模式。
      口  WindowManagerService.mLastStatusBarVisibility,在WMS中保存了焦点窗口所
            期望的SystemUiVisibility,其值等同于PhoneStatusBar.mSystemUiVisibility。当
            PhoneWindowManager因为某种原因希望清除某些标记(如SYSTEM_UL_FLAG_
            HIDE_NAVIGATION)时,WMS会以此成员的内容为基础,在删除了Phone-
            WindowManager 希望清除的标记之后通过updateStatusBarVisibilityLocked)将修正
            后的SystemUiVisibility保存到每一个WindowState之中并将这一变化通知窗口所在进
            程的ViewRootlmpl。

7.5.2SystemUIVisibility 发挥作用

      接下来讨论每一种SystemUTVisibility标记是如何发挥作用的。

1.SYSTEM_UL_FLAG_LOW_PROFILE

      处理这一标记的位置位于PhoneStatusBar.setSystemUiVisibility()中。当PhoneStatusBar.
mSystemUiVisibility中包含这一标记时,PhoneStatusBar会通过分别调用PhoneStatusBar.
setStatusBarLowProfile()和NavigationBarView.setLowProfile()两个方法使状态栏与导航栏进入
低辨识度模式。

      其中PhoneStatusBar.setStatusBarLowProfile()会将容纳通知信息的@id/notification_icon_area、
容纳系统状态图标的@id/statuslcons、显示信号信息的@id/signal_cluster、显示电
量信息的@id/battery以及显示系统时钟的@id/clock这5个控件的透明度在0或0.5与1之间
切换,使状态栏进人或退出低辨识度模式。

      在低辨识度模式下,@idbattery与@id/clock的透明度其实是0.5即半透明状态,其他
三个控件的透明度是0,即完全不可见。

     NavigationBarView.setLowProfile()则通过设置容纳4个虚拟按键的@id/nav_buttons以
及容纳三个灰色园点的@id/lights _out这两个控件的透明度来完成低辨识度模式的进入或退
出。在低辨识度模式下@id/nav_buttons的透明度为0,而@id/lights out的透明度为1。在正
常模式下@id/nav buttons的透明度为1,而@id/lights out的透明度为0。

2.SYSTEM_ULFLAG_HIDE_NAVIGATION

      对SYSTEM_UI_FLAG_HIDE_NAVIGATION的处理位于窗口布局过程的开端;
PhoneWindowManager.beginLayoutLw()。通过检查mLastSystemUiFlags中是否存在这一
标记并更改导航栏窗口的可见性。
导航栏可见性的更改会进一步影响作为窗口布局准绳的一
些矩形的计算。参考其代码:
[Phone WindowManager.java-->PhoneWindowManager.beginLayoutLw()]
public void beginLayoutIw (boolean ispefaultDisplay,int displaywidth
                        ,int displayHeight,int displayRotation){
    ......
    //①检查mLastSystemUiFlags中是否存在此标记。倘若存在则标记navVisible为false
    boolean navVisible =
            (mLastSystemUiFlags & View.SYSTEM_UI_FLAG_HTDE_NAVIGATION) == 0;
    ......//向WMS添加或移除用于截获用户触摸事件的FakeWindow
    /*若mCanHideNavigationBar为false,则强制导航栏显示。mCanHideNavigationBar被设
      置于PhonewindowManager.setInitialDisplaysize()中。mCanHideNavigationBar标记是否
      允许标记SYSTEM_UI_FLAG_HIDE_NAVIGATION隐藏导航栏。

      当SytemUI采用状态栏与导航栏分离式的布局方案时,mCanHideNavigationBar永远为true。因为
      横屏情况下导航栏位于屏幕的右侧,那么隐藏它可以为横屏下的宽屏视频播放提供更大的空间。
      而当SystemUI采用集成状态栏与导航栏为系统栏的方案时,会计算屏幕的宽高比。当宽高比大于16:9时,
      将会使得mCanHideNavigationBar为true,以致在横屏下的宽屏视频播放时可以充分利用屏幕的高度
*/
    navVisible |=! mCanHideNavigationBar;
    if(mNaviqationBar != null){
        //确定导航栏的方向
        mNavigationBaronBottom =
                (!mNavigationBarCanMove || displayWidth         if(mNavigationBaronBottom){
            //计算导航栏的ParentFrame,保存在mTmpNavigationFrame
            int top = displayHeight - mNavigationBarHeightForRotation[displayRotation];
            mTmpNavigationPrame.set(0,top,displaywidth,displayHeight);

            /*②根据navVisible设置导航栏窗口的可见性,并据此调整作为窗口布局准绳包括
              Dock、Reatricted、System、Content几个矩形
*/
            //更新Stable和StableFullscreen矩形,它与导航栏的窗口可见性无关
            mStableBottom = mStableFullscreenBottom = mTmpNavigationFrame.top;

            if(navVisible){
                //通过调用windowstate.showLw()显示导航栏的窗口
                mNavigationBar.showLw(true);

                //更新Dock以及Restricted矩形
                mDockBottom = mTmpNavigationFrame.top;
                mRestrictedScreenHeight = mDockBottom - mDockTop;

            }else {
                //通过WindowState.hideLw()方法隐藏导航栏的窗口
                mNavigationBar.hiderw(true);

            }
            if (navVisible &&!mNavigationBar.isAnimatingLw()){
                //更新System矩形
                mSystemBottom=mTmpNavigationFrame.top;

            }
        }else{
            .....//当导航栏位于屏幕右侧时的处理方式与导航栏位于屏幕底部时的处理方式类似
        }
        //更新Content矩形
        mContentTop=mCurTop=mDockTop;
        mContentBottom=mcurBottom = mDockBottom;
        mContentLeft=mCurLeft=mDockLeft;
        mContentRight=mCurRight=mDockRight;
    }
    ......
}
      首先,PhoneWindowManager.begineLayout()通过WindowState.showLw()/hideLw()进行导
航栏窗口的显示与隐藏。这两个方法是在WMS范畴内隐藏或显示现有窗口的一个十分便捷
的方法。注意hideLw()只是将窗口隐藏,而不是将窗口从WMS中移除。
      另外,焦点窗口声明SYSTEM_UI_FLAG_HIDE_ NAVIGATION标记进而影响导航
栏的可见性之后,会影响在PhoneWindowManager中作为窗口布局准绳的几个矩形:Dock、
Restricted、System以及Content
。因此在一个窗口中声明这个标记会对所有窗口的布局造成影响。

      另外,通过canHideNavigationBar在PhoneWindowManager.setlnitialDisplaySize()方法中的
计算过程可以体会到,Android极其不希望可以隐藏导航栏。因为导航栏所提供的BACK键、
HOME键为用户提供了离开当前程序的途径(尤其是HOME键,通过第5章关于事件派发
的知识可以知道,HOME键可以强制AMS启动桌面程序)。而声明了SYSTEM_UI_FLAG_
HIDE_NAVIGATION标记并使之隐藏导航栏的往往就是当前应用程序。为了避免恶意应用程
序无限期地隐藏导航栏,PhoneWindowManager通过FakeWindow机制为用户提供了一种绕过
当前应用程序快速呼出导航栏的方法。

      FakeWindow顾名思义,就是假窗口。经过本书第4章及第5章的介绍可知,一个窗口拥有
两个重要特点:一是拥有一块Surface用于显示内容,另一个拥有一个InputWindowHandle用于
接收用户事件。而FakeWindow则只有InputWindowHandle却没有Surface。
      当PhoneWindowManager隐藏导航栏时,它会通过WindowManagerService.addFakeWindow()
方法创建一个窗口类型为TYPE_HIDDEN_NAV_CONSUMER的FakeWindow—
mHideNavFakeWindow。TYPE_HIDDEN_NAV_CONSUMER类型在所有窗口类型中拥有最高
的Z序,因此它将覆盖在所有可能的窗口之上。由于FakeWindow 拥有InputWindowHandle,
所以用户在屏幕上的任何点击操作都会被 mHideNavFakeWindow截获。PhoneWindowManager
会在mHideNavFakeWindow 截获用户的点击之后强行将 SYSTEM_UL_FLAG_HIDE_NAVIGATION
标记从WindowManagerService.mLastStatusBarVisibility中移除,并重新对所有窗口进行
布局使得导航栏再次出现在用户面前。

      参考PhoneWindowManager创建 FakeWindow的代码:
[Phone WindowManager.java-->PhoneWindowManager.beginLayoutLw()]
public void beginLayoutLw(boolean isDefaultDisplay,int displaywidth
                        ,int displayHeight,int displayRotation){
    /*通过检查SYSTEM_UI_FLAG _HIDE_NAVIGATION标记以及mCanHideNavigationBar计算
      navVisible
*/
    if(navVisible){
        //①倘若导航栏可见,则移除FakeWindow。因为用作呼出导航栏的它已经不再需要了
        if(mHideNavFakewindow != null){
            mHideNavFakewindow.dismiss();
            mHideNavFakewindow = null;
        }
    }else if (mlideNavFakewindow == null){
        /*②倘若导航栏不可见,则创建FakeWindow。新的FakeWindow被保存在mHideNavFakeWindow中。
          注意mHideNavInputEventReceiverFactory参数,作为一个工厂类它可以用来创建一个
          InputEventReceiver,用于接收FakeWindow所截获的用户事件
*/
        mHideNavFakeWindow = mWindowManagerFuncs.addFakewindow(
                        mHandler.getlooper(),mlHideNavInputEventReceiverFactory,
                        "hidden nav",Windolanager.LayoutParams.TYPE HIDDEN NAV CONSUMER,
                        0,false,false,true);
    }
    ...... //进行导航栏的显示或隐藏
}
      可见创建与移除FakeWindow的方法十分简单,其中最值得讨论的便是
mHideNavInputEventReceiverFactory 所创建的InputEventReceiver是如何呼出已经隐藏的导航栏。
      PhoneWindowManager 实现InputEventReceiver的一个子类HideNavlnputEventReceiver
并在mHideNavInputEventReceiverFactory中创建它。
      参考HideNavlnpuEventReceiver.onlnputEvent()的代码:
[PhoneWindowManager java-->HideNavlnputEventReceiver.onlnputEvent()]
public void onInputEvent(InputEvent event){
    boolean handled=false;
    try{
        //HideNavInputEventReceiver只对触摸事件的ACTION_DOWN感兴趣
        if(event instanceof Motiongvent
                &&(event.getSource() & InputDevice.SOURCE_ CLASS_POINTER)!=0){
            final MotionEvent motionEvent = (MotionEvent)event;
            if (motionEvent.getAction() == MotionEvent.ACTION_DOWN){
                boolean changed=false;
                synchronized(mLock){
                    /*①首先刷新mResettingSystemUiFlags。
                      mResettingSystemUiFlags用于在系统现有的SystemuiVisibility中
                      清除所有会对状态栏/导航栏的可见性产生影响的标记
*/
                    int newVal = mResettingSystemUiFlags |
                                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                                View.SYSTEM_UI_FLAG_LOW_PROFILE |
                                View.SYSTEM_UI_FLAG_FULLSCREEN;
                    if (mResettingsystemuiFlags != newVal){
                        mResettingSystemUiFlags = newVal;
                        changed = true;
                    }
                    /*②刷新mForceClearedSystemUiFlags。
                      mForceClearedSystemUiFlags用于在随后的一段时间内对会导致隐藏导航栏的
                      SYSTEM_UI_FAG_HIDE_NAVIGATION标记进行屏蔽
*/
                    newVal = mForceClearedSystemUiFlags |
                            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
                    if (mForceClearedsystemUiFlags != newVal){
                        mForceClearedSystemUiFlags = newVal;
                        /*在一秒之后从mForceClearedSystemUiFlags中删除
                          SYSTEM_UI_FLAG_HIDE_NAVIGATION,以取消对此标记的屏蔽
*/
                        miHandler.postDelayed(new Runnable(){
                            @override gublic void run(){
                                    synchronized (mLock){
                                        mForceClearedsystemuiFlags &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
                                    }
                                    mWindowiManagerFuncs.reevaluateStatusBarVisibility();
                                }
                            },1000);
                    }
                }
                //③调用WMS.reevaluatestatuaBarViaibility()方法刷新SystemUiVisibility
                if(changed)(
                    mWindowManagerFuncs.reevaluateStatusBarVisibility();
                }
            }
        }
    }finally {.......}
}
当HideNavInputEventReceiver收到用户点击事件之后主要做了三个方面的工作:
 刷新mResettingSystemUiFlags。被刷新的mResettingSystemUiFlags将包含会影响状态
      栏或导航栏的可见性的三个标记:SYSTEM_UI_FLAG_LOW_PROFILE、SYSTEM_
      UI_FLAG_HIDE_NAVIGATION以及SYSTEM_UI_FLAG_FULLSCREEN。当窗口
      通过WMS.relayoutWindow()对窗口进行重新布局,或WMS调用PhoneWindow-
      Manager.adjustSystemUiVisibilityLw()时,mResetingSystemUiFlags会将这些标记从中
      移除。

口  刷新mForceClearedSystemUiFlags。mForceClearedSystemUiFlags 只负责清除 SYSTEM_
      UI_FLAG_HIDE_NAVIGATION。mForceClearedSystemUiFlags的存在是为了阻止恶
      意程序不断隐藏导航栏以阻挠用户对导航栏进行操作。本小节随后将对其原理进行
      介绍。

口  调用WMS.reevaluateStatusBarVisibility()。这会诱使WMS 调用PhoneWindowManager.
      adjustSystemUiVisibilityLw()方法使得PhoneWindowManager 将mResettingSystemUiFlags
      以及mForceClearedSystemUiFlags 中定义的标记从WMS.mLastStatusBarVisibility中删除,
      然后通过WMS.updateSystemUiVisibility()方法将删除这些标记之后的SystemUiVisibility
      更新至每一个窗口的WindowState以及窗口所在进程的ViewRootlmpl,最后进行一次
      performLayoutAndPlaceSurfacesLocked(),使得新的SystemUiVisibility在布局过程中
      生效(在beginLayoutLw()和finishPostLayoutLw()中)

      参考WMS.reevaluateStatusBarVisibility()的实现:
[WindowManagerService.java-->WindowManagerService.reevaluateStatusBarVisibility()]
public void reevaluateStatusBarVisibility(){
    synchronized (mMindowMap){
        /*①对mLastStatusBarVlaibility进行过滤。
          PhoneWindowManager.adjustSystemuiVisibilityLw()将删除mLaststatusBarvisibility
          中的那些定义在mReaettingsyatemuiFlags以及mForceClearedSystemUiFlags中的标记
*/
        int visibility=
                mPolicy.adjustSystemuiVisibilityLw(mLaststatusBarVisibility);
        /*②把过滤后的SyaemuiVlaibility更新到每一个窗口的WindowState以及窗口所在进程的VlewRootImpl
          于是WindowState以及控件树中的控件所保存的SystemurVisibility都会被更新
*/
        updatestatusBarVisibilityLocked(visibility);
        /*③进行一次完整的窗口布局操作。在这个过程中,新的SystemuiVisibility将通过
          PhoneWindowManager的beginlayoutLw()finishPostlayoutLw()以及updatesystemoiVisibility()
          三个方法生效。
          最后PhoneStatusBar通过WMS.statusBarVisibilityChanged()将新的SystemuiVisibility
          保存在WMS.mLastStatusBarVisibility
*/
        performLayoutAndPlacesurfacesLocked();
    }
}
      当用户在FakeWindow上产生点击后,WMS对mLastStatusBarVisibility进行过滤
从而得到 不包含 导致状态栏与导航栏不可见标记的  SystemUiVisibility,并将其推送到每
一个窗口(包括WindowState以及ViewRootlmpl),然后启动一次完整的窗口布局过程使
得PhoneWindowManager使用新的SystemUiVisibility对窗口进行布局以及设置状态栏与导
航栏的可见性。在这个过程中,PhoneWindowManager.updateSystemUiVisibility()会将新的
SystemUiVisibility 发送给PhoneStatusBar,而PhoneStatusBar通过调用WindowManager.
statusBarVisibilityChanged()方法将新的SystemUiVisibility 保存到WMS的mLastStatusBarVisibility中。

简单来说,通过FakeWindow呼出被隐藏的导航栏是从mLastStatusBarVisibility开始,通过过
滤、通知、布局之后再把新的SystemUiVisibility 保存到mLastStatusBarVisibility 这样一个环形
的过程。

      在这当中还需要讨论的是mResettingSystemUiVisibilitymForceClearedSystemUiVisibility的区别。
考察所有它们所生效的代码都如下所示:
      newVisibility = visibility & ~mResettingsystemUiFlags & ~mForceClearedSystemUiFlags;
      从这一点上来说,而这没有任何区别,它们都用来阻止某些标记出现在SystemUTVisibility

中。它们唯一的区别是——何时重置,即何时终止它们对特定标记的阻止行为。
      首先是mResettingSystemUiFlags。在PhoneWindowManager.adjustSystemUiVisibilityLw()
中有如下代码:

[Phone WindowManager java-->PhoneWindowManager.adjustSystemUiVisibilityLw()]
public int adjustSystemUiVisibilityLw(int visibility){
    /*mResettingsystemUiFlags取消对某一个标记的阻止行为的时机是
      系统中的SystemuIVisibility中不包含这一标记
*/
    mResettingSystemUiFlags &= visibility;
    return visibility &~mResettingsystemUiFlags
                      &~mEorceClearedSystemUiFlags;
}
      以SYSTEM_UI_FLAG_LOW_PROFILE为例介绍这一行代码的工作原理。假设WMS.
mLastStatusBarVisibility中包含这一标记,并且导航栏处于隐藏状态。当用户点击FakeWindow
之后,WMS会第一次调用PhoneWindowManager.adjustSystemUiVisibilityLw()方
法,此时由于这一标记存在,mResettingSystemUiFlags &= visibility这一条语句会使
mResettingSystemUiVisibility仍然保持这一标记,从而将返回值中的这一标记去
除。标记去除后的SystemUIVisibility 最终通过PhoneStatusBar调用WMS.statusBar-
VisibilityChanged()方法再次进入WMS,而WMS会再次调用PhoneWindowManager.
adjustSystemUiVisibilityLw(),此时SystemUiVisibility中已经不再包含SYSTEM_UI_FLAG_
LOW_PROFILE标记了,因此mResettingSystemUiFlags &= visibility这一条语句会使得
mResettingSystemUiFlags移除这一标记,因此取消了对这一标记的阻止。在此之后,应
用程序可以立刻重新声明SYSTEM_UI_FLAG_LOW_PROFILE。也就是说,mResettting-
SystemUiVisibility
存在的意义就是为了当用户点击FakeWindow后呼出状态栏与导航栏,它并
不阻止应用程序随后对状态栏或导航栏进行隐藏的尝试。
      而mForceClearedSystemUiFlags被清空的位置在HideNavInputEventReceiver.
onInputEvent()方法所抛出的那个Runnable中。也就是说,一旦FakeWindow呼出了导航栏,那
么在随后的1秒内,mForceClearedSystemUiFlags中将会持续保持对SYSTEM_UI FLAG_
HIDE_NAVIGATION的阻止行为。或者说,在这1秒的时间内导航栏不会再次被隐藏。
mForceClearedSystemUiVisibility存在的意义就是为了防止恶意程序不断通过声明SYSTEM_
UI_FLAG_HIDE_NAVIGATION而使得用户无法对导航栏进行操作。

      总结一下SYSTEM_UI_FLAG_HIDE_NAVIGATION对系统的影响。当焦点窗口拥有这一
标记时,导航栏会被隐藏,作为窗口布局准绳的几个矩形会被重新计算进而影响所有窗口的
布局结果。当这一标记是导航栏被隐藏时,一个FakeWindow覆盖在所有窗口之上。一旦用
户点击屏幕时,导航栏会被重新呼出,SYSTEM_UI_FLAG_HIDE_NAVIGATION标记会被强
制移除,并且在随后的1秒内,应用程序无法再通过这一标记对导航栏进行隐藏操作。

3.SYSTEM_UI_FLAG_FULLSCREEN

      首先需要澄清的是,千万不要被这个标记的名字误导,这一标记仅仅用来隐藏状态栏,
而不是同时隐藏状态栏与导航栏。这一标记的目的是希望用户能够更多地将注意力专注在
其应用程序的内容上,并占用状态栏的空间。对此标记进行处理的位置位于PhoneWindow-
Manager.finishPostLayoutLw)中。参考相关代码:
[Phone WindowManager.java-->PhoneWindowManager.finishPostLayoutLw()]
public int finishPostLayoutPolicyLw(){
    int changes=0;
    boolean topIsFullscreen = false;
    /*①会影响状态栏可见性的窗口不是焦点窗口,而是mTopFullscreenOpaqueWindowState
      即显示在最上面的全屏窗口。
这与SYSTEM_UI_FLAG_HIDE_NAVIGATTON标记不同*/
    final WindowManager.LayoutParams lp=(mTopFullscreenOpaqueWindowstate!=null)
                                ?mTopFullscreenopaqueWindowState.getAttrs()
                                :null;
    .......
    if(mStatusBar != null){
        if(mForcestatusBar || mForceStatusBarFromkeyguard){
            /*②强制状态栏显示。
              倘若mTopFullscreenOpaquewindowstate之上有窗口在其flags中声明FLAG_FORCE
              NOT_FULLSCREEN标记,那么状态栏将会被强制显示。此时SYSTEM_UI_FLAG_FULLSCREEN
              会被忽略
*/
            if(mStatusBar.showLw(true))changes |= FINISH_LAYOUT_REDO_LAYOUT;
        }else if(mTopFullscreenopaquewindowstate!=null){
            /*③检查是否需要隐藏状态栏。可见SYSTEM_UI_FLAG_FULLSCREEN
              只是隐藏状态栏的方法之一
*/
            topIsFullscreen =
                        (lp.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)!=0
                        ||(mLastsystemuiFlags & View.SYSTEM_UI_FLAG_FULISCREEN)!=0;
            if(topIsFullscreen){
                //通过Windowstate.hideLw()隐藏状态栏
                if(mStatusBar.hideLw(true)){
                    changes |= FINISH_LAYOUT_ REDO_ LAYOUT;
                    ......
                }
            }else {
                //通过Windowstate.showLw()显示状态栏
                if(mStatusBar.showLw(true))
                    changes |= FINISH_LAYOUT_REDO_LAYOUT;
            }
        }
    }
    ......//对锁屏的操作
    /*④如有必要,重新开始一次布局。
      当状态栏的可见性发生变化之后,说明作为窗口布局准绳的矩形需要重新计算,
      所以返回的changes会包含FINISH_LAYOUT_REDO_LAYOUT标记以重新启动一次布局。在新的布局过程中
      phonewindowManager.begineLayoutLw()会对布局准绳进行修改
*/
    return changes;
}
      首先与SYSTEM_UI_FLAGHIDE_NAVIGATION所不同的是,只有当显示在最上面的全
屏窗口声明此标记时才会导致隐藏状态栏。这个区别的存在其实非常好理解。隐藏导航栏会
使得用户无法发送虚拟按键,因而焦点窗口是这一行为的利益攸关方。所以Android将这一
权利赋予焦点窗口。而隐藏状态栏的目的是让用户将注意力集中在窗口所显示的内容上,并
占用状态栏所处的空间,因而仅当一个可以充满屏幕的窗口才能从中获利。所以Android将
这一权利赋予显示在最上面的全屏窗口。
      这一个区别也导致两者处理位置的不同。对SYSTEM_UI_FLAG_HIDE_NAVIGATION
来说,确定焦点窗口非常简单而且与布局无关,因此WMS可以在布局之前便将焦点窗
口确定下来并保存到PhoneWindowManager。于是PhoneWindowManager可以在布局一
开始的beginLayoutLw()中从焦点窗口中获取这一标记,并隐藏/显示状态栏进而修改
布局准绳,最后再对每个窗口进行布局。
这一过程如顺流而下十分自然。对SYSTEM_
UI_FLAG_FULLSCREEN
来说,显示在最上面的全屏窗口的确定依赖于布局结果,因此
PhoneWindowManager必须在完成一轮布局之后才能得知哪一个窗口的 SYSTEM_UL_FLAG_
FULLSCREEN 需要进行处理。所以对SYSTEM_UL_FLAG_FULLSCREEN的处理位于布局结
尾处的finishPostLayoutLw()
并且当状态栏可见性发生变化后通过在返回值中增加FINISH_
LAYOUT_REDO_LAYOUT标记导致重新布局,并在新的布局过程的begineLayoutLw()中根
据状态栏当前的可见性重新计算作为窗口布局准绳的矩形,然后重新布局所有窗口

      另外,除了SYSTEM_UI_FLAG_FULLSCREEN之外,窗口还可以通过在LayoutParams.
flags 添加
FLAG_FULLSCREEN标记完成对状态栏的隐藏。二者有什么区别呢?经过上一节
对SYSTEM_UI_FLAG_HIDE_NAVIGATION的探讨可知,SYSTEM_UI_FLAG_FULLSCREEN
这一标记的存在性并不稳定,因此它适用于需要在用户处于特定交互的情况下临时地隐藏状
态栏。对于那些需要持续隐藏状态栏的情况(例如全屏游戏),不会被PhoneWindowManager
清除的
FLAG_FULLSCREEN是一个更好的选择。

      显而易见,SYSTEM_UI_FLAG_HIDE_NAVIGATION 与SYSTEM_UI_FLAG_FULLSCREEN由
于会改变作为窗口布局准绳的矩形的位置和尺寸,因此它们会对所有参与布局的窗口
产生影响。

4.SYSTEM_UI_FLAG_LAYOUT_STABLE

      这个标记并不会对状态栏或导航栏的可见性产生任何影响,它只会对声明这一标记的窗
口的布局产生影响。

      由于状态栏与导航栏所在的区域不属于窗口的内容区。于是窗口的ContentFrame和
Frame之间所产生的差异会导致一个Contentlnset发送到ViewRootlmpl中。控件在绘制的过
程中,需要绕开Contentlnset所在的区域,以免其被状态栏或导航栏所覆盖。当状态栏在隐藏
与显示两种状态之间切换时,会使得ContentFrame不断变化,进而使得控件所绘制的内容上
下跳动。

      为了避免这种令人不快的跳动,窗口可以通过声明SYSTEM_UI_FLAG_LAYOUT_STABLE
标记以获得一个稳定的(STABLE)的ContentFrame
从而使得状态栏在显示与隐藏
之间切换时不会对窗口的绘制产生影响。
由于这一标记主要用于描述窗口针对状态栏的布局
行为,因此此标记仅对那些声明了与状态栏相关的标记的窗口才有效SYSTEM_UI_FLAG_
LAYOUT_STABLE需要配合以下标记使用:

      口  在LayoutParams.flags中指定了FLAG_LAYOUT_IN_SCREEN。
      口  声明了SYSTEM_UI_FLAG_HIDE_NAVIGATION_BAR。
      口  声明了SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN。

      这三个标记的共同特点是PhoneWindowManager为声明它们的窗口提供了充满屏幕顶
部的ParentFrame,使得这种窗口顶部有可能会被状态栏所覆盖。因而 SYSTEM_UL_FLAG_
LAYOUT_STABLE才有存在的必要。

       PhoneWindowManager 会在其applyStableConstraints()方法中检测这一标记并修改窗口的
ContentFrame。
参考其代码,其中参数r为窗口的ContentFrame。
[Phone WindowManager.java-->PhoneWindowManager.applyStableConstraints()]
private void applyStableConstraints(int sysui,int fl,Rect r){
    //检查窗口的SyatemuIVisibility中是否声明了SYSTEM_UI_FLAG_LAYOUT_STABLE
    if((sysui & view.SYSTEM_UI_FLAG_LAYOUT_STABE)!=0){
        //根据LayoutParams.flags中是否有FLAG_FULLSCREEN标记会返回不同的ContentFrame
        if((fl & FLAG_EULLSCREEN)!= O){
            //①当窗口声明FLAG_PULLSCRREN时,ContentPrame以stableFullscreen矩形为准
            if(r.left             if(r.top             if{r.right>mstableFullscreenRight) r.right-mStableFullscreenRight;
            if(r.bottom>mstableFullscreenBottom) r.bottom=mstablepullscreenBottom;
        }else{
            //②当窗口没有声明FLAG_FULLSCREEN时,ContentFrame以stable矩形为准
            if(r.left< mstableleft) r.left=mstableLeft;
            if(r.top             if (r.right>mstableRight) r.right=mStableRight;
            if(r.bottom>mstableBottom) r.bottom=mstableBottom;
        }
    }
}
      可见,一旦窗口声明了SYSTEM_UI_FLAG_LAYOUT_STABLE标记,那么它的ContentFrame
都会按照两种Stable矩形进行校正。这两种Stable矩形都不会随着状态栏的可见性的变化而
变化。因此窗口的控件树可以获得一个稳定的Contentlnsets,进而避免状态栏可见性变化时所
产生的内容跳动。

      Stable矩形StableFullscreen矩形之间唯一的区别在于它们的top值不同。Stable矩
的top值为状态栏的高度(无论状态栏显示与否),因此当窗口没有同时声明FLAG_
FULLSCREEN时,它将固定地获得一个将状态栏排除在外的Contentlnsets,因此,无论状态
栏显示与否都不会遮挡窗口控件树所绘制的内容。
StableFullscreen矩形的top值为0,因
此状态栏显示时会遮挡同时声明了FLAG_FULLSCREEN的窗口控件树所绘制的内容。

5.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

      这个标记不会对状态栏与导航栏的可见性产生任何影响。它只会影响声明这个标记
的窗口的布局。PhoneWindowManager在对声明这个标记的窗口进行布局时会假想导航
栏已经被隐藏
即导航栏所在的区域也会被用来进行窗口布局。与SYSTEM_UI_FLAG_
LAYOUT_STABLE相比,这个标记同时影响了ParentFrame、DisplayFrame以及ContentFrame。

      处理这个标记的代码位于PhoneWindowManager.layoutWindowLw()中。参考如下
代码:
[PhoneWindowManager.java-->PhoneWindowManager.layoutWindowLw()]
public void layoutwindowlw(Windowstate win,WindowManager.LayoutParams attrs,Windowstate attached){
    ......
    if((fl & (FLAG LAYOUT IN SCREEN | FLAG FULISCREEN | FLAG_LAYOUT_INSET_DECOR))
                    == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)
                    &&(sysUiFl & View.SYSTEM_UI_ELAG_FULLSCREEN) == 0){
        if{.......){
            ......
        }else if (mcanHideNavigationBar
                    &&(sysUiF1 & View.SYSTEM_UI_FLAG_LAYOUT_ HIDE NAVIGATION)!= 0
                    && attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDON
                    && attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW}{
            /*①对声明SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION的窗口来说,
              其ParentFrame、DisplayFrame都来自Unrestricted矩形
*/
            pf.left = df.left = munrestrictedScreenLeft;
            pf.top = df.top = mUnrestrictedScreenTop;
            pf.right = df.right = munrestrictedscreenteft + munreatrictedscreenwidth;
            pf.bottom = df.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
        }else{
            //②未声明此标记的窗口,其ParentFrame、DisplayFrame来自Restricted矩形
            pf.left = df.left = mRestrictedScreenLeft;
            pf.top = df.top = mRestrictedscreenTop;
            pf.right = df.right = mRestrictedScreeneftt + mRestrictedScreenlidth;
            pf.bottom = df.bottom = mRestrictedScreenTop + mRestrictedScreenHeight;
        }
    }
    ......
}
      可见SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION标记决定窗口的ParentFrame
以及DisplayFrame 选择Restricted矩形还是
Unrestricted矩形。这两个矩形唯一的差别就是,
Unrestricted矩形为整块屏幕,而
Restricted矩形则不包括导航栏。因此,这一标记的效果是使
得窗口的布局延展到整个屏幕。为了防止声明这一标记的窗口覆盖导航栏,因此要求声明这
一标记的窗口必须来自应用程序,以保证窗口的Z序位于导航栏以下。

      另外,参考代码最外层的if语句指出了上述行为的限定条件,即窗口同时在LayoutParams.
flags中声明FLAG_LAYOUT_IN_SCREEN以及FLAG_LAYOUT_INSET_DECOR,但是没有
声明FLAG_FULLSCREEN,也没有声明 SYSTEM_UI_FLAG_FULLSCREEN。当不满足上述
条件时,SYSTEM_UI_FLAG_FULLSCREEN不仅仅会修改DisplayFrame以及ParentFrame,
还会使用Unrestricted矩形作为窗口ContentFrame。

      产生这种不同的核心原因在于FLAG_LAYOUT_INSET_DECOR。这一标记表示声明它的
窗口需要通过Contentlnset知道状态栏与导航栏相对于窗口的位置与尺寸,以便在绘制时做出
相应的处理。所以上述所引用的代码中,
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
标记仅仅影响ParentFrame以及DisplayFrame,而ContentFrame依然按照默认的(考虑状态栏
与导航栏的位置)计算方式进行。于是ContentInsets便可以反映出状态栏与导航栏相对于窗口
的位置和尺寸。

6.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

      这一标记并不会影响状态栏与导航栏的可见性。但是影响声明它的窗口的布局。这使得
PhoneWindowManager对声明此标记的窗口进行布局时假设状态栏已经隐藏,但是导航栏依然
处于显示状态。
因此这个标记会使得窗口的ParentFrame、DisplayFrame以及ContentFrame使
用Restricted矩形进行布局。于是窗口可以被布局在状态栏之下,并且不会因为状态栏的存在
而产生Contentinsets。对这一标记的处理也位于
PhoneWindowManager.layoutWindowLw()中。
读者可以自行研究。

7.总结

      本节共介绍了6种SystemUIVisibility标记的处理过程及其所能产生的效果。
      前三种都会对状态栏以及导航栏产生实质性影响,并且会影响所有窗口的布局结果。而
后三种 SYSTEM UI FLAG LAYOUT STABLE/HIDE NAVIGATION/FULLSCREEN会对声明
它们的窗口布局产生影响。
      另外后三者同时影响ParentFrame、DisplayFrame以及ContentFrame中的一个或多个。
那么当这些标记同时存在时是否存在互斥或者优先级呢?通过分析PhoneWindowManager.
layoutWindowLw()方法的代码不难看出,HIDE_NAVIGATION与FULLSCREEN之间存在
互斥,当两者同时存在时,使用HIDE_NAVIGATION对窗口进行布局。而STABLE与其他
两者可以共同发生作用。STABLE会对HIDE_NAVIGATION或FULLSCREEN所计算出的
ContentFrame 进行修正。

7.5.3SystemUIVisibility总结

      关于SystemUIVisibility的话题便介绍到这里。SystemUIVisibility是应用程序与状态栏和
导航栏进行互动的主要手段,主要用于影响状态栏与导航栏的可见性以及窗口的布局方式。
另外,对系统开发者来说,相对扩充StatusBarManager的接口以扩展状态栏/导航栏与应用程
序的交互行为,扩展SystemUTVisibility 标记并在PhoneStatusBar.setSystemUiVisibilityO中进行
相应处理无疑是一个更加方便而且影响较小的途径。

7.6本章小结

      本章详细介绍了SystemUI中最常用也是最重要的两个功能:状态栏与导航栏的工作原理。
根据屏幕尺寸不同,Android实现两套状态栏与导航栏。本章仅讨论了其中一套应用于小屏幕
设备上的状态栏与导航栏分离布局的方案。
      读者需要理解状态栏与导航栏运行于一个名为SystemUIService的由 system_server 进程通
过Context.startService0方式启动的常规Android服务中,并且通过WindowManager.addViewO
创建它们的窗口。这种由系统服务启动一个常规服务,并在这个常规服务中创建窗口的工作
模式在Android系统中并不是独此一家。第8章中所介绍的Android壁纸也使用了这种工作模
式,而且负责管理壁纸的WallpaperManagerService与负责壁纸显示的WallpaperService之间的
交互以及窗口创建过程更加复杂。因此深入理解状态栏与导航栏的启动、窗口创建以及它们
与StatusBarManagerService进行通信的方式对于继续学习第8章的内容有很大帮助作用。

 

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

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

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