第7章 深入理解SystemUI(节选)

最后更新于:2022-04-01 22:04:47

# 第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还提供了PowerUI和RingtonePlayer两个服务。前者负责监控系统的剩余电量并在必要时为用户显示低电警告,后者则依托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(newRunnable() {     publicvoid 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<N; 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会将操作       状态栏的请求发送给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();     /*notificationKeys和StatusBarNotification则存储了需要显示在状态栏的通知区中通知信息。       前者存储了一个用Binder表示的通知发送者的ID列表。而notifications则存储了通知列表。二者       通过索引号一一对应。关于通知的工作原理将在7.2.2节介绍 */    ArrayList<IBinder> notificationKeys = newArrayList<IBinder>();    ArrayList<StatusBarNotification> notifications                                     = newArrayList<StatusBarNotification>();     /*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<IBinder> binders = new ArrayList<IBinder>();     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);     // 初始化状态栏的系统状态区的状态图标列表。关于系统状态区的工作原理将在7.2.3节介绍     finalResources 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<IBinder> notificationKeys,List<StatusBarNotification> notifications,         intswitches[], List<IBinder> 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<IBinder,StatusBarNotification> 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中还包含了ImageWallpaper、RecentPanel以及TakeScreenshotService等功能的实现。它们是Service、Activity等标准的Android应用程序组件,并且互相独立。对这些功能感兴趣的使用者可以通过startService()/startActivity()等方式方便地启动相应的功能。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d567b3f2a1f.png) 图 7 - 1 SystemUI的体系结构 在本章将主要介绍SystemUI中最常用的状态栏、导航栏以及RecentPanel的实现。ImageWallpaper将在第8章中进行详细地介绍。而SystemUI其他的功能读者可以自行研究。 ## 7.2 深入理解状态栏 如7.1.1节所述,SystemUI中存在两种状态栏与导航栏的实现——即状态栏与导航栏分离的布局的PhoneStatusBar以及状态栏与导航栏集成布局的TabletStatusBar两种。除了布局差异之外,二者并无本质上的差别,因此本节将主要介绍PhoneStatusBar下的状态栏的实现。 作为一个将所有信息集中显示的场所,状态栏对需要显示的信息做了以下的五个分类。 + 通知信息:它可以在状态栏左侧显示一个图标以引起用户的主意,并在下拉卷帘中为用户显示更加详细的信息。这是状态栏所能提供的信息显示服务之中最灵活的一种功能。它对信息种类以及来源没有做任何限制。使用者可以通过StatusBarManagerService所提供的接口向状态栏中添加或移除一条通知信息。 + 时间信息:显示在状态栏最右侧的一个小型数字时钟,是一个名为Clock的继承自TextView的控件。它监听了几个和时间相关的广播:ACTION_TIME_TICK、ACTION_TIME_CHANGED、ACTION_TIMEZONE_CHANGED以及ACTION_CONFIGURATION_CHANGED。当其中一个广播到来时从Calendar类中获取当前的系统时间,然后进行字符串格式化后显示出来。时间信息的维护工作在状态栏内部完成,因此外界无法通过API修改时间信息的显示或行为。 + 电量信息:显示在数字时钟左侧的一个电池图标,用于提示设备当前的电量情况。它是一个被BatteryController类所管理的ImageView。BatteryController通过监听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()创建状态栏的窗口**    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所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d567b4184b4.png) 图 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所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d567b434b82.png) 图 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/notificationIcons。IconMerger继承自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。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d567b44bac3.png) 图 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作为参数用于指明希望移除的通知所具有的意图。
';