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中实现的。

源码位置:/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;
    }
}
    <!-- 需要移除的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之中。因此它是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中还有“消息中心”、“近期任务”等一些关键模块,这些内容就放到以后再做介绍吧。

目录
相关文章
|
11天前
|
Android开发 Swift iOS开发
深入探索iOS与Android操作系统的架构差异及其对应用开发的影响
在当今数字化时代,移动设备已经成为我们日常生活和工作不可或缺的一部分。其中,iOS和Android作为全球最流行的两大移动操作系统,各自拥有独特的系统架构和设计理念。本文将深入探讨iOS与Android的系统架构差异,并分析这些差异如何影响应用开发者的开发策略和用户体验设计。通过对两者的比较,我们可以更好地理解它们各自的优势和局限性,从而为开发者提供有价值的见解,帮助他们在这两个平台上开发出更高效、更符合用户需求的应用。
|
20天前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
20天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
20天前
|
Android开发 Swift iOS开发
探索iOS与安卓应用开发的差异性
探索iOS与安卓应用开发的差异性
41 2
|
27天前
|
传感器 XML IDE
探索安卓应用开发:从基础到进阶
【10月更文挑战第23天】在数字化时代的浪潮中,移动应用已成为人们日常生活的延伸。本文以安卓平台为例,深入浅出地介绍了如何从零开始构建一个安卓应用,涵盖了开发环境搭建、基本组件使用、界面设计原则以及进阶技巧等关键步骤。通过实例演示和代码片段,引导读者逐步掌握安卓应用开发的核心技能,旨在激发更多开发者对安卓平台的探索热情,并为初学者提供一条清晰的学习路径。
|
15天前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
26天前
|
开发工具 Android开发 Swift
探索iOS与安卓应用开发的异同点
【10月更文挑战第24天】本文通过比较iOS和安卓开发环境,旨在揭示两大移动平台在开发过程中的相似性与差异性。我们将探讨开发工具、编程语言、用户界面设计、性能优化及市场分布等方面,以期为开发者提供全面的视角。通过深入浅出的分析,文章将帮助读者更好地理解每个平台的独特之处及其对应用开发的影响。
|
6天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
11天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
13天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。