Android 車載應(yīng)用開發(fā)與分析(12) - SystemUI (一)

1.前言

Android 車載應(yīng)用開發(fā)與分析是一個系列性的文章贤旷,這個是第12篇,該系列文章旨在分析原生車載Android系統(tǒng)中核心應(yīng)用的實(shí)現(xiàn)方式,幫助初次從事車載應(yīng)用開發(fā)的同學(xué)享幽,更好地理解車載應(yīng)用開發(fā)的方式睹限,積累android系統(tǒng)應(yīng)用的開發(fā)經(jīng)驗(yàn)譬猫。

注意:本文的源碼分析部分非常的枯燥讯檐,最好還是下載android源碼然后對著看,逐步理順邏輯染服。
本文中使用的源碼基于android-11.0.0_r48
在線源碼可以使用下面的網(wǎng)址(基于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通俗的解釋就是系統(tǒng)的 UI别洪,在Android 系統(tǒng)中由SystemUI負(fù)責(zé)統(tǒng)一管理整個系統(tǒng)層的UI,它也是一個系統(tǒng)級應(yīng)用程序(APK)柳刮,但是與我們之前接觸過的系統(tǒng)應(yīng)用程序不同挖垛,SystemUI的源碼在/frameworks/base/packages/目錄下,而不是在/packages/目錄下秉颗,這也說明了SystemUI這個應(yīng)用的本質(zhì)上可以歸屬于framework層痢毒。

  • SystemUI

Android - Phone中SystemUI從源碼量看就是一個相當(dāng)復(fù)雜的程序,常見的如:狀態(tài)欄蚕甥、消息中心哪替、近期任務(wù)、截屏以及一系列功能都是在SystemUI中實(shí)現(xiàn)的菇怀。

源碼位置:/frameworks/base/packages/SystemUI

  • CarSystemUI

Android-AutoMotive 中的SystemUI相對手機(jī)中要簡單不少凭舶,目前商用車載系統(tǒng)中幾乎必備的頂部狀態(tài)欄、消息中心爱沟、底部導(dǎo)航欄在原生的Android系統(tǒng)中都已經(jīng)實(shí)現(xiàn)了帅霜。

源碼位置:frameworks/base/packages/CarSystemUI

雖然CarSystemUISystemUI的源碼位置不同,但是二者實(shí)際上是復(fù)用關(guān)系钥顽。通過閱讀CarSystemUI的Android.bp文件可以發(fā)現(xiàn)CarSystemUI在編譯時把SystemUI以靜態(tài)庫的方式引入進(jìn)來了义屏。

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開發(fā)者應(yīng)該都聽說SystemServer,它是Android framework中關(guān)鍵系統(tǒng)的服務(wù)蜂大,由Android系統(tǒng)最核心的進(jìn)程Zygotefork生成闽铐,進(jìn)程名為system_server。我們常說的ActivityManagerService奶浦、PackageManagerService兄墅、WindowManageService都是由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()代碼細(xì)節(jié)如下.從這里我們可以看出,SystemUI本質(zhì)就是一個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組件的創(chuàng)建,然后再調(diào)用各個SystemUIonStart()方法來繼續(xù)執(zhí)行子模塊的初始化基矮。

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]);
        // 調(diào)用各個子模塊的start()
        mServices[i].start();
        // 首次啟動時淆储,這里始終為false,不會被調(diào)用
        if (mBootCompleteCache.isBootComplete()) {
            mServices[i].onBootCompleted();
        }
    }
    mServicesStarted = true;
}

SystemUIApplicationOnCreate()方法中注冊了一個開機(jī)廣播家浇,當(dāng)接收到開機(jī)廣播后會調(diào)用SystemUIonBootCompleted()方法來告訴每個子模塊Android系統(tǒng)已經(jīng)完成開機(jī)本砰。

    @Override
    public void onCreate() {
        super.onCreate();
        Log.v(TAG, "SystemUIApplication created.");
        // 設(shè)置所有服務(wù)繼承的應(yīng)用程序主題。
        // 請注意钢悲,在清單中設(shè)置應(yīng)用程序主題僅適用于activity点额。這里是讓Service保持與主題設(shè)置同步。
        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 {
            // 我們不需要為正在執(zhí)行某些任務(wù)的子進(jìn)程啟動服務(wù)譬巫。
           ...
        }
    }

這里的SystemUI是一個抽象類咖楣,狀態(tài)欄、近期任務(wù)等等模塊都是繼承自SystemUI芦昔,通過這種方式可以很大程度上簡化復(fù)雜的SystemUI程序中各個子模塊創(chuàng)建方式诱贿,同時我們可以通過配置資源的方式動態(tài)加載需要的SystemUI模塊。

在實(shí)際的項目中開發(fā)我們自己的SystemUI時咕缎,這種初始化子模塊的方式是值得我們學(xué)習(xí)的珠十,不過由于原生的SystemUI使用了AOP框架 - Dagger來創(chuàng)建組件,所以SystemUI子模塊的初始化細(xì)節(jié)就不再介紹了凭豪。

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) {
    }

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

    protected void onBootCompleted() {
    }

總結(jié)一下,SystemUI的大致啟動流程可以歸納如下(時序圖語法并不嚴(yán)謹(jǐn)帖努,理解即可)

3.CarSystemUI 的啟動流程

之前也提到過CarSystemUI復(fù)用了手機(jī)SystemUI的代碼撰豺,所以CarSystemUI的啟動流程和SystemUI的是完全一致的。

這里就有個疑問拼余,CarSystemUI中需要的功能與SystemUI中是有差異的污桦,那么是這些差異化的功能是如何引入并完成初始化?以及一些手機(jī)的SystemUI才需要的功能是如何去除的呢匙监?

其實(shí)很簡單凡橱,在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;
    }
}
    <!-- 需要移除的Components. -->
    <string-array name="config_systemUIServiceComponentsExclude" translatable="false">
        <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.keyboard.KeyboardUI</item>
        <item>com.android.systemui.pip.PipUI</item>
        <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
        <item>com.android.systemui.LatencyTester</item>
        <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
        <item>com.android.systemui.SliceBroadcastRelayHandler</item>
        <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>
        <item>com.android.systemui.accessibility.WindowMagnification</item>
        <item>com.android.systemui.accessibility.SystemActions</item>
    </string-array>

    <!-- 新增的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模塊的源碼量極大变抽,全部分析一遍再寫成文章耗費(fèi)的時間將無法估計,這里結(jié)合我個人在車載方面的工作經(jīng)驗(yàn)氮块,揀出了一些在商用車載項目必備的功能绍载,來分析它們在原生系統(tǒng)中是如何實(shí)現(xiàn)的。

3.頂部狀態(tài)欄與底部導(dǎo)航欄

  • 頂部狀態(tài)欄

狀態(tài)欄是CarSystemUI中一個功能重要的功能滔蝉,它負(fù)責(zé)向用戶展示操作系統(tǒng)當(dāng)前最基本信息击儡,例如:時間、蜂窩網(wǎng)絡(luò)的信號強(qiáng)度蝠引、藍(lán)牙信息阳谍、wifi信息等。

  • 底部導(dǎo)航欄

在原生的車載Android系統(tǒng)中螃概,底部的導(dǎo)航按鈕由經(jīng)典的三顆返回矫夯、主頁、菜單鍵替換成如下圖所示的七顆快捷功能按鈕吊洼。從左到右依次主頁训貌、地圖、藍(lán)牙音樂冒窍、藍(lán)牙電話递沪、桌面、消息中心综液、語音助手款慨。


3.1 布局方式

  • 頂部狀態(tài)欄
    頂部狀態(tài)欄的布局方式比較簡單,如下圖所示:


布局文件的源碼就不貼了谬莹,量比較大檩奠,而且包含了許多的自定義View,如果不是為了學(xué)習(xí)如何自定義View閱讀的意義不大附帽。

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

  • 底部導(dǎo)航欄
    底部狀態(tài)欄的布局方式就更簡單了埠戳,如下圖所示:


不過比較有意思的是,導(dǎo)航欄士葫、狀態(tài)欄每個按鈕對應(yīng)的Action的intent都是直接定義在布局文件的xml中的乞而,這點(diǎn)或許值得參考。

<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在通過反射創(chuàng)建好CarNavigationBar后爪模,緊接就調(diào)用了start()方法,那么我們就從start()入手荚藻,開始UI的初始化流程屋灌。

在start()方法中,首先是向IStatusBarService中注冊一個CommandQueue应狱,然后執(zhí)行createNavigationBar()方法共郭,并把注冊的結(jié)果下發(fā)。

CommandQueue繼承自IStatusBar.Stub。因此它是IStatusBar的服務(wù)(Bn)端除嘹。在完成注冊后写半,這一Binder對象的客戶端(Bp)端將會保存在IStatusBarService之中。因此它是IStatusBarServiceBaseStatusBar進(jìn)行通信的橋梁尉咕。

IStatusBarService叠蝇,即系統(tǒng)服務(wù)StatusBarManagerService是狀態(tài)欄導(dǎo)航欄向外界提供服務(wù)的前端接口,運(yùn)行于system_server進(jìn)程中年缎。

注意:定制SystemUI時悔捶,我們可以不使用 IStatusBarService 和 IStatusBar 來保存 SystemUI 的狀態(tài)

// 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()中依次執(zhí)行buildNavBarWindows()buildNavBarContent()单芜、attachNavBarWindows()蜕该。

// CarNavigationBar
private void createNavigationBar(RegisterStatusBarResult result) {
    buildNavBarWindows();
    buildNavBarContent();
    attachNavBarWindows();
    // 如果注冊成功,嘗試設(shè)置導(dǎo)航條的初始狀態(tài)洲鸠。
if (result != null) {
        setImeWindowStatus(Display.DEFAULT_DISPLAY, result.mImeToken,
                result.mImeWindowVis, result.mImeBackDisposition,
                result.mShowImeSwitcher);
    }
}

下面依次介紹每個方法的實(shí)際作用堂淡。

  • buildNavBarWindows() 這個方法目的是創(chuàng)建出狀態(tài)欄的容器 - 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字面意思就是“死區(qū)”淤齐,它的作用是消耗沿導(dǎo)航欄頂部邊緣的無意輕擊。當(dāng)用戶在輸入法上快速輸入時袜匿,他們可能會嘗試點(diǎn)擊空格鍵更啄、“overshoot”,并意外點(diǎn)擊主頁按鈕居灯。每次點(diǎn)擊導(dǎo)航欄外的UI后祭务,死區(qū)會暫時擴(kuò)大(因?yàn)檫@是偶然點(diǎn)擊更可能發(fā)生的情況),然后隨著時間的推移怪嫌,死區(qū)又會縮幸遄丁(因?yàn)樯院蟮狞c(diǎn)擊可能是針對導(dǎo)航欄頂部的)。

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

  • buildNavBarContent()

這個方法目的是將狀態(tài)欄的實(shí)際View添加到上一步創(chuàng)建出的容器中岩灭,并對觸摸和點(diǎn)擊事件進(jìn)行初始化拌倍。

// 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。當(dāng)用戶導(dǎo)航到另一個窗口時噪径,旋轉(zhuǎn)控制器將焦點(diǎn)“椭簦”在這里。這也用于防止wrap-around.找爱。
view.addView(new FocusParkingView(mContext), 0);

    mCachedViewMap.put(type, view);
    return mCachedViewMap.get(type);
}
  • attachNavBarWindows()

最后一步梗顺,將創(chuàng)建的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;
    }
}

簡單總結(jié)一下车摄,UI初始化的流程圖如下寺谤。


3.3 關(guān)鍵功能

3.3.1 打開/關(guān)閉消息中心

在原生車載Android中有兩種方式打開消息中心分別是仑鸥,1.通過點(diǎn)擊消息中心按鈕,2.通過手勢下拉狀態(tài)欄变屁。

我們先來看第一種實(shí)現(xiàn)方式 眼俊,通過點(diǎn)擊按鈕展開消息中心。


CarNavigationBarController中對外暴露了一個可以注冊監(jiān)聽回調(diào)的方法敞贡,CarNavigationBarController會把外部注冊的監(jiān)聽事件會傳遞到CarNavigationBarView中泵琳。

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

當(dāng)CarNavigationBarView中的notifications按鈕被按下時誊役,就會將打開消息中心的消息回調(diào)給之前注冊進(jìn)來的接口。

// 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();
    }
}

消息中心的控制器在接收到回調(diào)消息后谷市,根據(jù)需要執(zhí)行展開消息中心面板的方法即可

// NotificationPanelViewMediator
mCarNavigationBarController.registerNotificationController(
        new CarNavigationBarController.NotificationsShadeController() {
            @Override
            public void togglePanel() {
                mNotificationPanelViewController.toggle();
            }
            
            // 這個方法用于告知外部類蛔垢,當(dāng)前消息中心的面板是否處于展開狀態(tài)
            @Override
            public boolean isNotificationPanelOpen() {
                return mNotificationPanelViewController.isPanelExpanded();
            }
        });

再來看第二種實(shí)現(xiàn)方式 ,通過下拉手勢展開消息中心迫悠,這也是我們最常用的方式鹏漆。


實(shí)現(xiàn)思路第一種方式一樣,CarNavigationBarController中對外暴露了一個可以注冊監(jiān)聽回調(diào)的方法创泄,接著會把外部注冊的監(jiān)聽事件會傳遞給CarNavigationBarView艺玲。

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

這次在CarNavigationBarView中則是攔截了觸摸事件的分發(fā),如果當(dāng)前消息中心已經(jīng)展開鞠抑,則CarNavigationBarView直接消費(fèi)觸摸事件饭聚,后續(xù)事件不再對外分發(fā)。如果當(dāng)前消息中心沒有展開搁拙,則將觸摸事件分外給外部秒梳,這里的外部就是指消息中心中的TopNotificationPanelViewMediator

// CarNavigationBarView

// 用于連接通知的打開/關(guān)閉手勢
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();

        // 將觸摸事件轉(zhuǎn)發(fā)到狀態(tài)欄窗口箕速,以便在需要時拖動窗口(Notification shade)
mStatusBarWindowTouchListener.onTouch(this, ev);

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

TopNotificationPanelViewMediator在初始化過程中就向CarNavigationBarController注冊了觸摸事件的監(jiān)聽酪碘。

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

最終狀態(tài)欄的觸摸事件會在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 占用應(yīng)用的顯示區(qū)域

不知道你有沒有這樣的疑問兴垦,既然頂部的狀態(tài)欄和底部導(dǎo)航欄都是通過WindowManager.addView()顯示到屏幕上,那么打開應(yīng)用為什么會自動“讓出”狀態(tài)欄占用的區(qū)域呢字柠?

主要原因在于狀態(tài)欄的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頂部的狀態(tài)欄WindowType是 TYPE_STATUS_BAR_ADDITIONAL

底部導(dǎo)航欄的WindowType是 TYPE_NAVIGATION_BAR_PANEL

4. 總結(jié)

SystemUI在原生的車載Android系統(tǒng)是一個極其復(fù)雜的模塊募谎,考慮多數(shù)從手機(jī)應(yīng)用轉(zhuǎn)行做車載應(yīng)用的開發(fā)者并對SystemUI的了解并不多扶关,本篇介紹了CarSystemUI的啟動、和狀態(tài)欄的實(shí)現(xiàn)方式数冬,希望能幫到正在或以后會從事SystemUI開發(fā)的同學(xué)节槐。

除此以外搀庶,車載SystemUI中還有“消息中心”、“近期任務(wù)”等一些關(guān)鍵模塊铜异,這些內(nèi)容就放到以后再做介紹吧哥倔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市揍庄,隨后出現(xiàn)的幾起案子咆蒿,更是在濱河造成了極大的恐慌,老刑警劉巖蚂子,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沃测,死亡現(xiàn)場離奇詭異,居然都是意外死亡食茎,警方通過查閱死者的電腦和手機(jī)蒂破,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來别渔,“玉大人附迷,你說我怎么就攤上這事“ッ模” “怎么了喇伯?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拨与。 經(jīng)常有香客問我稻据,道長,這世上最難降的妖魔是什么截珍? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任攀甚,我火速辦了婚禮,結(jié)果婚禮上岗喉,老公的妹妹穿的比我還像新娘秋度。我一直安慰自己,他們只是感情好钱床,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布荚斯。 她就那樣靜靜地躺著,像睡著了一般查牌。 火紅的嫁衣襯著肌膚如雪事期。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天纸颜,我揣著相機(jī)與錄音兽泣,去河邊找鬼。 笑死胁孙,一個胖子當(dāng)著我的面吹牛唠倦,可吹牛的內(nèi)容都是我干的称鳞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼稠鼻,長吁一口氣:“原來是場噩夢啊……” “哼冈止!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起候齿,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤熙暴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后慌盯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體周霉,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年润匙,在試婚紗的時候發(fā)現(xiàn)自己被綠了诗眨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡孕讳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出巍膘,到底是詐尸還是另有隱情厂财,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布峡懈,位于F島的核電站璃饱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肪康。R本人自食惡果不足惜荚恶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望磷支。 院中可真熱鬧谒撼,春花似錦、人聲如沸雾狈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽善榛。三九已至辩蛋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間移盆,已是汗流浹背悼院。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咒循,地道東北人据途。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓绞愚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親昨凡。 傳聞我的和親對象是個殘疾皇子爽醋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容