Android 車載應用開發與分析(12) - SystemUI (一)

語言: CN / TW / HK

1.前言

Android 車載應用開發與分析是一個系列性的文章,這個是第12篇,該系列文章旨在分析原生車載Android系統中核心應用的實現方式,幫助初次從事車載應用開發的同學,更好地理解車載應用開發的方式,積累android系統應用的開發經驗。

注意:本文的原始碼分析部分非常的枯燥,最好還是下載android原始碼然後對著看,逐步理順邏輯。 本文中使用的原始碼基於android-11.0.0_r48 線上原始碼可以使用下面的網址(基於android-11.0.0_r21) http://aospxref.com/android-11.0.0_r21/xref/frameworks/base/packages/CarSystemUI/ http://aospxref.com/android-11.0.0_r21/xref/frameworks/base/packages/SystemUI/

2.車載 SystemUI

2.1 SystemUI 概述

SystemUI通俗的解釋就是系統的 UI,在Android 系統中由SystemUI負責統一管理整個系統層的UI,它也是一個系統級應用程式(APK),但是與我們之前接觸過的系統應用程式不同,SystemUI的原始碼在/frameworks/base/packages/目錄下,而不是在/packages/目錄下,這也說明了SystemUI這個應用的本質上可以歸屬於framework層。

  • SystemUI

Android - Phone中SystemUI從原始碼量看就是一個相當複雜的程式,常見的如:狀態列、訊息中心、近期任務、截圖以及一系列功能都是在SystemUI中實現的。

原始碼位置:/frameworks/base/packages/SystemUI

  • CarSystemUI

Android-AutoMotive 中的SystemUI相對手機中要簡單不少,目前商用車載系統中幾乎必備的頂部狀態列、訊息中心、底部導航欄在原生的Android系統中都已經實現了。

原始碼位置:frameworks/base/packages/CarSystemUI

雖然CarSystemUISystemUI的原始碼位置不同,但是二者實際上是複用關係。通過閱讀CarSystemUI的Android.bp檔案可以發現CarSystemUI在編譯時把SystemUI以靜態庫的方式引入進來了。

android.bp原始碼位置:/frameworks/base/packages/CarSystemUI/Android.bp

android_library { name: "CarSystemUI-core", ... static_libs: [ "SystemUI-core", "SystemUIPluginLib", "SystemUISharedLib", "SystemUI-tags", "SystemUI-proto", ... ], ... }

2.2 SystemUI 啟動流程

Android開發者應該都聽說SystemServer,它是Android framework中關鍵系統的服務,由Android系統最核心的程序Zygotefork生成,程序名為system_server。我們常說的ActivityManagerServicePackageManagerServiceWindowManageService都是由SystemServer啟動的。

而在ActivityManagerService完成啟動後(SystemReady),SystemServer就會去著手啟動SystemUI

SystemServer 的原始碼路徑:frameworks/base/services/java/com/android/server/SystemServer.java

``` mActivityManagerService.systemReady(() -> { Slog.i(TAG, "Making services ready");

        t.traceBegin("StartSystemUI");
        try {
            startSystemUi(context, windowManagerF);
        } catch (Throwable e) {
            reportWtf("starting System UI", e);
        }
        t.traceEnd();
    }, t);

```

startSystemUi()程式碼細節如下.從這裡我們可以看出,SystemUI本質就是一個Service,通過Pm獲取到的Component 是com.android.systemui/.SystemUIService。

private static void startSystemUi(Context context, WindowManagerService windowManager) { PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class); Intent intent = new Intent(); intent.setComponent(pm.getSystemUiServiceComponent()); intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING); //Slog.d(TAG, "Starting service: " + intent); context.startServiceAsUser(intent, UserHandle.SYSTEM); windowManager.onSystemUiStarted(); }

startSystemUi()中啟動SystemUIService,在SystemUIServiceoncreate()方法中再通過SystemUIApplication.startServicesIfNeeded()來完成SystemUI的元件的初始化。

SystemUIService 原始碼位置:/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java

// SystemUIService @Override public void onCreate() { super.onCreate(); Slog.e("SystemUIService", "onCreate"); // Start all of SystemUI ((SystemUIApplication) getApplication()).startServicesIfNeeded(); ... }

startServicesIfNeeded()中,通過SystemUIFactory獲取到配置在config.xml中每個子模組的className。

SystemUIApplication 原始碼位置:/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java

``` // SystemUIApplication public void startServicesIfNeeded() { String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponents(getResources()); startServicesIfNeeded("StartServices", names); }

// SystemUIFactory /* Returns the list of system UI components that should be started. / public String[] getSystemUIServiceComponents(Resources resources) { return resources.getStringArray(R.array.config_systemUIServiceComponents); } ```

<!-- SystemUI Services: The classes of the stuff to start. --> <string-array name="config_systemUIServiceComponents" translatable="false"> <item>com.android.systemui.util.NotificationChannels</item> <item>com.android.systemui.keyguard.KeyguardViewMediator</item> <item>com.android.systemui.recents.Recents</item> <item>com.android.systemui.volume.VolumeUI</item> <item>com.android.systemui.stackdivider.Divider</item> <item>com.android.systemui.statusbar.phone.StatusBar</item> <item>com.android.systemui.usb.StorageNotification</item> <item>com.android.systemui.power.PowerUI</item> <item>com.android.systemui.media.RingtonePlayer</item> <item>com.android.systemui.keyboard.KeyboardUI</item> <item>com.android.systemui.pip.PipUI</item> <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item> <item>@string/config_systemUIVendorServiceComponent</item> <item>com.android.systemui.util.leak.GarbageMonitor$Service</item> <item>com.android.systemui.LatencyTester</item> <item>com.android.systemui.globalactions.GlobalActionsComponent</item> <item>com.android.systemui.ScreenDecorations</item> <item>com.android.systemui.biometrics.AuthController</item> <item>com.android.systemui.SliceBroadcastRelayHandler</item> <item>com.android.systemui.SizeCompatModeActivityController</item> <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item> <item>com.android.systemui.theme.ThemeOverlayController</item> <item>com.android.systemui.accessibility.WindowMagnification</item> <item>com.android.systemui.accessibility.SystemActions</item> <item>com.android.systemui.toast.ToastUI</item> </string-array>

最終在startServicesIfNeeded()中通過反射完成了每個SystemUI元件的建立,然後再呼叫各個SystemUIonStart()方法來繼續執行子模組的初始化。

``` private SystemUI[] mServices;

private void startServicesIfNeeded(String metricsPrefix, String[] services) { if (mServicesStarted) { return; } mServices = new SystemUI[services.length]; ...

final int N = services.length;
for (int i = 0; i < N; i++) {
    String clsName = services[i];
    if (DEBUG) Log.d(TAG, "loading: " + clsName);
    try {
        SystemUI obj = mComponentHelper.resolveSystemUI(clsName);
        if (obj == null) {
            Constructor constructor = Class.forName(clsName).getConstructor(Context.class);
            obj = (SystemUI) constructor.newInstance(this);
        }
        mServices[i] = obj;
    } catch (ClassNotFoundException
            | NoSuchMethodException
            | IllegalAccessException
            | InstantiationException
            | InvocationTargetException ex) {
        throw new RuntimeException(ex);
    }

    if (DEBUG) Log.d(TAG, "running: " + mServices[i]);
    // 呼叫各個子模組的start()
    mServices[i].start();
    // 首次啟動時,這裡始終為false,不會被呼叫
    if (mBootCompleteCache.isBootComplete()) {
        mServices[i].onBootCompleted();
    }
}
mServicesStarted = true;

} ```

SystemUIApplicationOnCreate()方法中註冊了一個開機廣播,當接收到開機廣播後會呼叫SystemUIonBootCompleted()方法來告訴每個子模組Android系統已經完成開機。

``` @Override public void onCreate() { super.onCreate(); Log.v(TAG, "SystemUIApplication created."); // 設定所有服務繼承的應用程式主題。 // 請注意,在清單中設定應用程式主題僅適用於activity。這裡是讓Service保持與主題設定同步。 setTheme(R.style.Theme_SystemUI);

    if (Process.myUserHandle().equals(UserHandle.SYSTEM)) {
        IntentFilter bootCompletedFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
        bootCompletedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
        registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (mBootCompleteCache.isBootComplete()) return;
                if (DEBUG) Log.v(TAG, "BOOT_COMPLETED received");
                unregisterReceiver(this);
                mBootCompleteCache.setBootComplete();
                if (mServicesStarted) {
                    final int N = mServices.length;
                    for (int i = 0; i < N; i++) {
                        mServices[i].onBootCompleted();
                    }
                }
            }
        }, bootCompletedFilter);
           ...
    } else {
        // 我們不需要為正在執行某些任務的子程序啟動服務。
       ...
    }
}

```

這裡的SystemUI是一個抽象類,狀態列、近期任務等等模組都是繼承自SystemUI,通過這種方式可以很大程度上簡化複雜的SystemUI程式中各個子模組建立方式,同時我們可以通過配置資源的方式動態載入需要的SystemUI模組。

在實際的專案中開發我們自己的SystemUI時,這種初始化子模組的方式是值得我們學習的,不過由於原生的SystemUI使用了AOP框架 - Dagger來建立元件,所以SystemUI子模組的初始化細節就不再介紹了。

SystemUI的原始碼如下,方法基本都能見名知意,就不再介紹了。

``` public abstract class SystemUI implements Dumpable { protected final Context mContext;

public SystemUI(Context context) {
    mContext = context;
}

public abstract void start();

protected void onConfigurationChanged(Configuration newConfig) {
}

// 非核心功能,可以不用關心
@Override
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
}

protected void onBootCompleted() {
}

```

總結一下,SystemUI的大致啟動流程可以歸納如下(時序圖語法並不嚴謹,理解即可)

3.CarSystemUI 的啟動流程

之前也提到過CarSystemUI複用了手機SystemUI的程式碼,所以CarSystemUI的啟動流程和SystemUI的是完全一致的。

這裡就有個疑問,CarSystemUI中需要的功能與SystemUI中是有差異的,那麼是這些差異化的功能是如何引入並完成初始化?以及一些手機的SystemUI才需要的功能是如何去除的呢?

其實很簡單,在SystemUI的啟動流程中我們得知,各個子模組的className是通過SystemUIFactorygetSystemUIServiceComponents()獲取到的,那麼只要繼承SystemUIFactory並重寫getSystemUIServiceComponents()就可以了。

``` public class CarSystemUIFactory extends SystemUIFactory {

@Override
protected SystemUIRootComponent buildSystemUIRootComponent(Context context) {
    return DaggerCarSystemUIRootComponent.builder()
            .contextHolder(new ContextHolder(context))
            .build();
}

@Override
public String[] getSystemUIServiceComponents(Resources resources) {
    Set<String> names = new HashSet<>();
    // 先引入systemUI中的components
    for (String s : super.getSystemUIServiceComponents(resources)) {
        names.add(s);
    }
    // 再移除CarsystemUI不需要的components
    for (String s : resources.getStringArray(R.array.config_systemUIServiceComponentsExclude)) {
        names.remove(s);
    }
    // 最後再新增CarsystemUI特有的components
    for (String s : resources.getStringArray(R.array.config_systemUIServiceComponentsInclude)) {
        names.add(s);
    }

    String[] finalNames = new String[names.size()];
    names.toArray(finalNames);

    return finalNames;
}

} ```

``` com.android.systemui.recents.Recents com.android.systemui.volume.VolumeUI com.android.systemui.stackdivider.Divider com.android.systemui.statusbar.phone.StatusBar com.android.systemui.keyboard.KeyboardUI com.android.systemui.pip.PipUI com.android.systemui.shortcut.ShortcutKeyDispatcher com.android.systemui.LatencyTester com.android.systemui.globalactions.GlobalActionsComponent com.android.systemui.SliceBroadcastRelayHandler com.android.systemui.statusbar.notification.InstantAppNotifier com.android.systemui.accessibility.WindowMagnification com.android.systemui.accessibility.SystemActions

<!-- 新增的Components. -->
<string-array name="config_systemUIServiceComponentsInclude" translatable="false">
    <item>com.android.systemui.car.navigationbar.CarNavigationBar</item>
    <item>com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier</item>
    <item>com.android.systemui.car.window.SystemUIOverlayWindowManager</item>
    <item>com.android.systemui.car.volume.VolumeUI</item>
</string-array>

```

通過以上方式,就完成了CarSystemUI子模組的替換。

由於CarSystemUI模組的原始碼量極大,全部分析一遍再寫成文章耗費的時間將無法估計,這裡結合我個人在車載方面的工作經驗,揀出了一些在商用車載專案必備的功能,來分析它們在原生系統中是如何實現的。

3.頂部狀態列與底部導航欄

  • 頂部狀態列

狀態列是CarSystemUI中一個功能重要的功能,它負責向用戶展示作業系統當前最基本資訊,例如:時間、蜂窩網路的訊號強度、藍芽資訊、wifi資訊等。

  • 底部導航欄

在原生的車載Android系統中,底部的導航按鈕由經典的三顆返回、主頁、選單鍵替換成如下圖所示的七顆快捷功能按鈕。從左到右依次主頁、地圖、藍芽音樂、藍芽電話、桌面、訊息中心、語音助手。

3.1 佈局方式

  • 頂部狀態列 頂部狀態列的佈局方式比較簡單,如下圖所示:

佈局檔案的原始碼就不貼了,量比較大,而且包含了許多的自定義View,如果不是為了學習如何自定義View閱讀的意義不大。

原始碼位置:frameworks/base/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml

  • 底部導航欄 底部狀態列的佈局方式就更簡單了,如下圖所示:

不過比較有意思的是,導航欄、狀態列每個按鈕對應的Action的intent都是直接定義在佈局檔案的xml中的,這點或許值得參考。

<com.android.systemui.car.navigationbar.CarNavigationButton android:id="@+id/grid_nav" style="@style/NavigationBarButton" systemui:componentNames="com.android.car.carlauncher/.AppGridActivity" systemui:highlightWhenSelected="true" systemui:icon="@drawable/car_ic_apps" systemui:intent="intent:#Intent;component=com.android.car.carlauncher/.AppGridActivity;launchFlags=0x24000000;end" systemui:selectedIcon="@drawable/car_ic_apps_selected" />

3.2 初始化流程

SystemUI的啟動流程中,SystemUIApplication在通過反射建立好CarNavigationBar後,緊接就呼叫了start()方法,那麼我們就從start()入手,開始UI的初始化流程。

在start()方法中,首先是向IStatusBarService中註冊一個CommandQueue,然後執行createNavigationBar()方法,並把註冊的結果下發。

CommandQueue繼承自IStatusBar.Stub。因此它是IStatusBar的服務(Bn)端。在完成註冊後,這一Binder物件的客戶端(Bp)端將會儲存在IStatusBarService之中。因此它是IStatusBarServiceBaseStatusBar進行通訊的橋樑。

IStatusBarService,即系統服務StatusBarManagerService是狀態列導航欄向外界提供服務的前端介面,運行於system_server程序中。

注意:定製SystemUI時,我們可以不使用 IStatusBarService 和 IStatusBar 來儲存 SystemUI 的狀態

``` // CarNavigationBar

private final CommandQueue mCommandQueue; private final IStatusBarService mBarService;

@Override public void start() { ... RegisterStatusBarResult result = null; try { result = mBarService.registerStatusBar(mCommandQueue); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } ... createNavigationBar(result); ... } ```

createNavigationBar()中依次執行buildNavBarWindows()buildNavBarContent()attachNavBarWindows()

// CarNavigationBar private void createNavigationBar(RegisterStatusBarResult result) { buildNavBarWindows(); buildNavBarContent(); attachNavBarWindows(); // 如果註冊成功,嘗試設定導航條的初始狀態。 if (result != null) { setImeWindowStatus(Display.DEFAULT_DISPLAY, result.mImeToken, result.mImeWindowVis, result.mImeBackDisposition, result.mShowImeSwitcher); } }

下面依次介紹每個方法的實際作用。

  • buildNavBarWindows() 這個方法目的是創建出狀態列的容器 - navigation_bar_window。

``` // CarNavigationBar private final CarNavigationBarController mCarNavigationBarController;

private void buildNavBarWindows() { mTopNavigationBarWindow = mCarNavigationBarController.getTopWindow(); mBottomNavigationBarWindow = mCarNavigationBarController.getBottomWindow(); ... }

// CarNavigationBarController private final NavigationBarViewFactory mNavigationBarViewFactory;

public ViewGroup getTopWindow() { return mShowTop ? mNavigationBarViewFactory.getTopWindow() : null; }

// NavigationBarViewFactory public ViewGroup getTopWindow() { return getWindowCached(Type.TOP); }

private ViewGroup getWindowCached(Type type) { if (mCachedContainerMap.containsKey(type)) { return mCachedContainerMap.get(type); }

ViewGroup window = (ViewGroup) View.inflate(mContext,
        R.layout.navigation_bar_window, /* root= */ null);
mCachedContainerMap.put(type, window);
return mCachedContainerMap.get(type);

} ```

navigation_bar_window 是一個自定義View(NavigationBarFrame),它的核心類是DeadZone.

DeadZone字面意思就是“死區”,它的作用是消耗沿導航欄頂部邊緣的無意輕擊。當用戶在輸入法上快速輸入時,他們可能會嘗試點選空格鍵、“overshoot”,並意外點選主頁按鈕。每次點選導航欄外的UI後,死區會暫時擴大(因為這是偶然點選更可能發生的情況),然後隨著時間的推移,死區又會縮小(因為稍後的點選可能是針對導航欄頂部的)。

navigation_bar_window 原始碼位置:/frameworks/base/packages/SystemUI/res/layout/navigation_bar_window.xml

  • buildNavBarContent()

這個方法目的是將狀態列的實際View新增到上一步創建出的容器中,並對觸控和點選事件進行初始化。

``` // CarNavigationBar private void buildNavBarContent() { mTopNavigationBarView = mCarNavigationBarController.getTopBar(isDeviceSetupForUser()); if (mTopNavigationBarView != null) { mSystemBarConfigs.insetSystemBar(SystemBarConfigs.TOP, mTopNavigationBarView); mTopNavigationBarWindow.addView(mTopNavigationBarView); }

mBottomNavigationBarView = mCarNavigationBarController.getBottomBar(isDeviceSetupForUser());
if (mBottomNavigationBarView != null) {
    mSystemBarConfigs.insetSystemBar(SystemBarConfigs.BOTTOM, mBottomNavigationBarView);
    mBottomNavigationBarWindow.addView(mBottomNavigationBarView);
}
...

}

// CarNavigationBarController public CarNavigationBarView getTopBar(boolean isSetUp) { if (!mShowTop) { return null; }

mTopView = mNavigationBarViewFactory.getTopBar(isSetUp);
setupBar(mTopView, mTopBarTouchListener, mNotificationsShadeController);
return mTopView;

}

// 初始化 private void setupBar(CarNavigationBarView view, View.OnTouchListener statusBarTouchListener, NotificationsShadeController notifShadeController) { view.setStatusBarWindowTouchListener(statusBarTouchListener); view.setNotificationsPanelController(notifShadeController); mButtonSelectionStateController.addAllButtonsWithSelectionState(view); mButtonRoleHolderController.addAllButtonsWithRoleName(view); mHvacControllerLazy.get().addTemperatureViewToController(view); }

// NavigationBarViewFactory public CarNavigationBarView getTopBar(boolean isSetUp) { return getBar(isSetUp, Type.TOP, Type.TOP_UNPROVISIONED); }

private CarNavigationBarView getBar(boolean isSetUp, Type provisioned, Type unprovisioned) { CarNavigationBarView view; if (isSetUp) { view = getBarCached(provisioned, sLayoutMap.get(provisioned)); } else { view = getBarCached(unprovisioned, sLayoutMap.get(unprovisioned)); }

if (view == null) {
    String name = isSetUp ? provisioned.name() : unprovisioned.name();
    Log.e(TAG, "CarStatusBar failed inflate for " + name);
    throw new RuntimeException(
            "Unable to build " + name + " nav bar due to missing layout");
}
return view;

}

private CarNavigationBarView getBarCached(Type type, @LayoutRes int barLayout) { if (mCachedViewMap.containsKey(type)) { return mCachedViewMap.get(type); } // CarNavigationBarView view = (CarNavigationBarView) View.inflate(mContext, barLayout, / root= / null); // 在開頭包括一個FocusParkingView。當用戶導航到另一個視窗時,旋轉控制器將焦點“停”在這裡。這也用於防止wrap-around.。 view.addView(new FocusParkingView(mContext), 0);

mCachedViewMap.put(type, view);
return mCachedViewMap.get(type);

} ```

  • attachNavBarWindows()

最後一步,將建立的View通過windowManger顯示到螢幕上。

``` private void attachNavBarWindows() { mSystemBarConfigs.getSystemBarSidesByZOrder().forEach(this::attachNavBarBySide); }

private void attachNavBarBySide(int side) { switch(side) { case SystemBarConfigs.TOP: if (mTopNavigationBarWindow != null) { mWindowManager.addView(mTopNavigationBarWindow, mSystemBarConfigs.getLayoutParamsBySide(SystemBarConfigs.TOP)); } break; case SystemBarConfigs.BOTTOM: if (mBottomNavigationBarWindow != null && !mBottomNavBarVisible) { mBottomNavBarVisible = true; mWindowManager.addView(mBottomNavigationBarWindow, mSystemBarConfigs.getLayoutParamsBySide(SystemBarConfigs.BOTTOM)); } break; ... break; default: return; } } ```

簡單總結一下,UI初始化的流程圖如下。

3.3 關鍵功能

3.3.1 開啟/關閉訊息中心

在原生車載Android中有兩種方式開啟訊息中心分別是,1.通過點選訊息中心按鈕,2.通過手勢下拉狀態列。

我們先來看第一種實現方式 ,通過點選按鈕展開訊息中心。

CarNavigationBarController中對外暴露了一個可以註冊監聽回撥的方法,CarNavigationBarController會把外部註冊的監聽事件會傳遞到CarNavigationBarView中。

/** 設定切換通知面板的通知控制器。 */ public void registerNotificationController( NotificationsShadeController notificationsShadeController) { mNotificationsShadeController = notificationsShadeController; if (mTopView != null) { mTopView.setNotificationsPanelController(mNotificationsShadeController); } ... }

CarNavigationBarView中的notifications按鈕被按下時,就會將開啟訊息中心的訊息回撥給之前註冊進來的介面。

// CarNavigationBarView @Override public void onFinishInflate() { ... mNotificationsButton = findViewById(R.id.notifications); if (mNotificationsButton != null) { mNotificationsButton.setOnClickListener(this::onNotificationsClick); } ... } protected void onNotificationsClick(View v) { if (mNotificationsShadeController != null) { mNotificationsShadeController.togglePanel(); } }

訊息中心的控制器在接收到回撥訊息後,根據需要執行展開訊息中心面板的方法即可

``` // NotificationPanelViewMediator mCarNavigationBarController.registerNotificationController( new CarNavigationBarController.NotificationsShadeController() { @Override public void togglePanel() { mNotificationPanelViewController.toggle(); }

        // 這個方法用於告知外部類,當前訊息中心的面板是否處於展開狀態
        @Override
        public boolean isNotificationPanelOpen() {
            return mNotificationPanelViewController.isPanelExpanded();
        }
    });

```

再來看第二種實現方式 ,通過下拉手勢展開訊息中心,這也是我們最常用的方式。

實現思路第一種方式一樣,CarNavigationBarController中對外暴露了一個可以註冊監聽回撥的方法,接著會把外部註冊的監聽事件會傳遞給CarNavigationBarView

// CarNavigationBarController public void registerTopBarTouchListener(View.OnTouchListener listener) { mTopBarTouchListener = listener; if (mTopView != null) { mTopView.setStatusBarWindowTouchListener(mTopBarTouchListener); } }

這次在CarNavigationBarView中則是攔截了觸控事件的分發,如果當前訊息中心已經展開,則CarNavigationBarView直接消費觸控事件,後續事件不再對外分發。如果當前訊息中心沒有展開,則將觸控事件分外給外部,這裡的外部就是指訊息中心中的TopNotificationPanelViewMediator

``` // CarNavigationBarView

// 用於連線通知的開啟/關閉手勢 private OnTouchListener mStatusBarWindowTouchListener;

public void setStatusBarWindowTouchListener(OnTouchListener statusBarWindowTouchListener) { mStatusBarWindowTouchListener = statusBarWindowTouchListener; }

@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mStatusBarWindowTouchListener != null) { boolean shouldConsumeEvent = mNotificationsShadeController == null ? false : mNotificationsShadeController.isNotificationPanelOpen();

    // 將觸控事件轉發到狀態列視窗,以便在需要時拖動視窗(Notification shade)

mStatusBarWindowTouchListener.onTouch(this, ev);

    if (mConsumeTouchWhenPanelOpen && shouldConsumeEvent) {
        return true;
    }
}
return super.onInterceptTouchEvent(ev);

} ```

TopNotificationPanelViewMediator在初始化過程中就向CarNavigationBarController註冊了觸控事件的監聽。

.// TopNotificationPanelViewMediator @Override public void registerListeners() { super.registerListeners(); getCarNavigationBarController().registerTopBarTouchListener( getNotificationPanelViewController().getDragOpenTouchListener()); }

最終狀態列的觸控事件會在OverlayPanelViewController中得到處理。

``` // OverlayPanelViewController public final View.OnTouchListener getDragOpenTouchListener() { return mDragOpenTouchListener; }

mDragOpenTouchListener = (v, event) -> { if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) { return true; } if (!isInflated()) { getOverlayViewGlobalStateController().inflateView(this); }

boolean consumed = openGestureDetector.onTouchEvent(event);
if (consumed) {
    return true;
}
// 判斷是否要展開、收起 訊息中心的面板
maybeCompleteAnimation(event);
return true;

}; ```

3.3.2 佔用應用的顯示區域

不知道你有沒有這樣的疑問,既然頂部的狀態列和底部導航欄都是通過WindowManager.addView()顯示到螢幕上,那麼開啟應用為什麼會自動“讓出”狀態列佔用的區域呢?

主要原因在於狀態列的Window的Type和我們平常使用的TYPE_APPLICATION是不一樣的。

``` private WindowManager.LayoutParams getLayoutParams() { WindowManager.LayoutParams lp = new WindowManager.LayoutParams( isHorizontalBar(mSide) ? ViewGroup.LayoutParams.MATCH_PARENT : mGirth, isHorizontalBar(mSide) ? mGirth : ViewGroup.LayoutParams.MATCH_PARENT, mapZOrderToBarType(mZOrder), WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, PixelFormat.TRANSLUCENT); lp.setTitle(BAR_TITLE_MAP.get(mSide)); lp.providesInsetsTypes = new int[]{BAR_TYPE_MAP[mBarType], BAR_GESTURE_MAP.get(mSide)}; lp.setFitInsetsTypes(0); lp.windowAnimations = 0; lp.gravity = BAR_GRAVITY_MAP.get(mSide); return lp; }

private int mapZOrderToBarType(int zOrder) { return zOrder >= HUN_ZORDER ? WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL : WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; } ```

CarSystemUI頂部的狀態列WindowType是 TYPE_STATUS_BAR_ADDITIONAL

底部導航欄的WindowType是 TYPE_NAVIGATION_BAR_PANEL

4. 總結

SystemUI在原生的車載Android系統是一個極其複雜的模組,考慮多數從手機應用轉行做車載應用的開發者並對SystemUI的瞭解並不多,本篇介紹了CarSystemUI的啟動、和狀態列的實現方式,希望能幫到正在或以後會從事SystemUI開發的同學。

除此以外,車載SystemUI中還有“訊息中心”、“近期任務”等一些關鍵模組,這些內容就放到以後再做介紹吧。