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_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
模塊的源碼量極大,全部分析一遍再寫成文章耗費的時間將無法估計,這里結合我個人在車載方面的工作經驗,揀出了一些在商用車載項目必備的功能,來分析它們在原生系統中是如何實現的。
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后,死區會暫時擴大(因為這是偶然點擊更可能發生的情況),然后隨著時間的推移,死區又會縮?。ㄒ驗樯院蟮狞c擊可能是針對導航欄頂部的)。
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中還有“消息中心”、“近期任務”等一些關鍵模塊,這些內容就放到以后再做介紹吧。