Android 車載應用開發與分析(12) - SystemUI (一)
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
中實現的。
- CarSystemUI
Android-AutoMotive 中的SystemUI
相對手機中要簡單不少,目前商用車載系統中幾乎必備的頂部狀態列、訊息中心、底部導航欄在原生的Android系統中都已經實現了。
雖然CarSystemUI
與SystemUI
的原始碼位置不同,但是二者實際上是複用關係。通過閱讀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系統最核心的程序Zygote
fork生成,程序名為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()
程式碼細節如下.從這裡我們可以看出,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
,在SystemUIService
的oncreate()
方法中再通過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
元件的建立,然後再呼叫各個SystemUI
的onStart()
方法來繼續執行子模組的初始化。
``` 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;
} ```
SystemUIApplication
在OnCreate()
方法中註冊了一個開機廣播,當接收到開機廣播後會呼叫SystemUI
的onBootCompleted()
方法來告訴每個子模組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是通過SystemUIFactory
的getSystemUIServiceComponents()
獲取到的,那麼只要繼承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_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
之中。因此它是IStatusBarService
與BaseStatusBar
進行通訊的橋樑。
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中還有“訊息中心”、“近期任務”等一些關鍵模組,這些內容就放到以後再做介紹吧。
- Android車載應用開發與分析(1) - Android Automotive概述與編譯
- 【Android R】車載 Android 核心服務 - CarService 解析
- 【Android R】車載 Android 核心服務 - CarPropertyService
- 車載Android程式設計師的2022年終總結與轉行建議
- 從應用工程師的角度再談車載 Android 系統
- Android 車載應用開發與分析(12) - SystemUI (一)
- RE: 從零開始的車載Android HMI(三) - SurfaceView
- RE: 從零開始的車載Android HMI(二) - Widget
- RE: 從零開始的車載Android HMI(一) - Lottie
- Android 車載應用開發與分析 (3)- 構建 MVVM 架構(Java版)
- Android 車載應用開發與分析 (4)- 編寫基於AIDL 的 SDK