# 第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()等方式方便地啟動相應的功能。

圖 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所示。

圖 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/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。

圖 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作為參數用于指明希望移除的通知所具有的意圖。