《深入理解Android 卷III》第七章 深入理解SystemUI

《深入理解Android 卷III》即将公布,作者是张大伟。此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分。在一个特别讲究颜值的时代,本书分析了Android 4.2中WindowManagerService、ViewRoot、Input系统、StatusBar、Wallpaper等重要“颜值绘制/处理”模块


第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而言,它更像是一个client。

因为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()等方式方便地启动相应的功能。

《深入理解Android 卷III》第七章 深入理解SystemUI

图 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入手能够非常easy地得知状态栏的控件树的结构:

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所看到的。

《深入理解Android 卷III》第七章 深入理解SystemUI

图 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所看到的。

《深入理解Android 卷III》第七章 深入理解SystemUI

图 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。

《深入理解Android 卷III》第七章 深入理解SystemUI

图 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类中包括例如以下几个用于描写叙述通知信息的keyword段。

·  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作为參数用于指明希望移除的通知所具有的意图。