拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android 11 CarLauncher源码解析与自定义车载桌面开发实战

先交代一下背景。之前我在车载方案公司做系统定制接手的第一件事就是把 Android 11 里的 CarLauncher 源码吃透然后基于它做一套车厂定制的桌面。说实话这个过程比预期复杂得多TaskView、Workspace、GridCardView、壁纸联动、焦点管理每一块都牵扯到系统级逻辑市面上的资料又少基本是啃源码加踩坑摸索出来的。这篇博文我会把 CarLauncher 的核心源码结构、关键机制以及我实际做自定义 Launcher 的完整思路和步骤分享出来。内容基于 Android 11API 30的 AOSP 源码适合正在做车机系统开发、Launcher 定制、framework 层应用开发的同学参考也适合想系统了解车载桌面是怎么工作的朋友阅读。我会尽量把代码、配置、踩坑点都写清楚不搞虚的。1. 为什么要研究 CarLauncher车载桌面的特殊战场1.1 车载桌面到底和手机桌面差在哪很多人觉得车载桌面不就是个“横屏手机桌面”吗实际上差别很大。手机上 Launcher 的核心是应用抽屉加桌面快捷方式用户体验围绕触控和流畅动画展开。但车载场景里用户是驾驶员和乘客首要目标是“安全”和“低干扰”界面元素要大、信息层级要少、焦点状态要清晰还得支持方向盘按键、旋钮这类非触控输入。这决定了车载桌面在设计上更强调卡片式任务管理。在 Android 11 的原生 CarLauncher 里主界面分成两大部分上部是一个大的任务卡片区TaskView把最近使用的应用、导航、媒体这类高频卡片放在最显眼的位置下部是固定快捷方式区域再往左滑动才是全部应用列表。整个布局逻辑是以“最快触达高频功能”为中心这和手机桌面的“个性化、自由摆放”逻辑完全不同。技术实现上也有明显差异。手机桌面通常跑在普通应用层但车载桌面往往需要读取系统任务栈、监听特定系统事件、控制壁纸、联动 SystemUI甚至参与系统按键事件分发。所以我一开始就明确照着手机 Launcher 的套路去改车载桌面是行不通的必须从 CarLauncher 的源码结构入手理解它的运行机制再在此基础上做定制。1.2 Android 11 CarLauncher 的架构分布CarLauncher 在 AOSP 里的源码路径是packages/apps/Car/Launcher这个目录下其实是一个标准的 Android 应用工程但它依赖android.car这个系统库所以它本质上属于 Car 系统级应用需要系统签名才能运行。在 Android 11 里CarLauncher 的主要架构大致是这样CarLauncher.java主 Activity负责整体的视图组装和生命周期管理。TaskView桌面顶部的大卡片区域展示最近任务和正在运行的应用。TaskViewControllerTaskView 的数据控制器负责从系统拉取任务列表、监听任务变化。GridCardViewTaskView 底层的网格卡片容器本质是一个 RecyclerView。Workspace快捷方式页面容器用于放置固定的应用快捷方式和页面滑动。AppsListViewController应用列表的控制器管理全部应用页的数据加载与展示。这里有个关键点是Android 11 把 TaskView 从 SystemUI 移到了 CarLauncher 里面。TaskView 早期是放在 SystemUI 里实现的但在 Android 11 上调整到了 Launcher 模块目的很明显就是把“任务卡片的展示与交互”下沉到桌面应用层让车厂定制桌面时有更大的自由度。我们做定制时就不用动 SystemUI 那一大坨代码了直接在 Launcher 里控制即可。理解了整体架构接下来我按源码顺序一层层拆。2. CarLauncher 源码核心结构拆解2.1 从 CarLauncher.java 入口看整体流程CarLauncher.java 是主 Activity入口逻辑非常直观。核心流程在onCreate里Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.car_launcher); mWorkspace findViewById(R.id.workspace); mTaskView findViewById(R.id.task_view); mAppsListViewController new AppsListViewController(this); mTaskViewController new TaskViewController(this); }布局文件car_launcher.xml大致是这样的结构com.android.car.carlauncher.CarLauncher ... com.android.car.carlauncher.workspace.Workspace android:idid/workspace ... / com.android.car.carlauncher.TaskView android:idid/task_view ... / /com.android.car.carlauncher.CarLauncher但这里有个容易忽略的细节TaskView并不是普通的一个 View而是一个组合视图负责承载热点任务区域Workspace则是一个继承自PagedView的容器负责快捷方式页面的横向滑动。两者的数据来源差异很大TaskView 的数据来自系统任务栈Workspace 的数据来自桌面数据库中的快捷方式。我在第一次读源码时先别急着抠某个控件细节而是沿着onCreate里的初始化顺序走一遍搞清楚每个 controller 是干什么的、监听哪些系统事件。这样后面改任何一块都不会乱。2.2 TaskView 与 TaskViewController卡片式热点区域的运作机制TaskView 是整个车载桌面里最核心的一个区域它展示的不是普通快捷方式而是把运行中的任务以卡片形式展示出来。这一块的机制在 Android 11 里经历了比较大改动值得细说。TaskView 本身是一个FrameLayout的子类内部通过TaskViewController访问任务数据。关键方法是// TaskView 内部 TaskViewController mTaskViewController; Override protected void onFinishInflate() { super.onFinishInflate(); mTaskViewController new TaskViewController(getContext()); mTaskViewController.setModel(new TaskViewModel()); }TaskViewController的核心职责是从系统的ActivityManager获取最近任务列表整理成TaskViewModel后交给 GridCardView 去渲染。在 Android 11 上由于分区存储和后台限制策略Launcher 能读取到的任务信息其实是经过系统过滤的只包含当前用户、当前显示设备上的任务。这个机制保证了在多用户、多屏的车载场景不会乱。TaskView 还有一层逻辑是处理卡片点击。用户点到某张卡片时不是简单startActivity而是通过任务的taskId把对应任务拉到前台mActivityManager.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME);这里用moveTaskToFront而不是重新启动 Activity是为了保留任务原有的状态避免导航、通话这类关键任务被重建。这个细节对车载场景非常重要——你肯定不希望驾驶过程中切回导航应用时导航任务被重启。2.3 GridCardView 的布局结构与数据绑定GridCardView 是 TaskView 内部实际展示卡片的容器。在 Android 11 的 CarLauncher 中它是一个 RecyclerView使用 GridLayoutManager 来做网格排列。我最初看到这里时有点疑惑为什么用网格而不是平铺的卡片流原因在于车载桌面要同时兼顾“最近任务卡片”和“固定快捷方式”两类内容而且屏幕横向空间充裕用网格可以在一屏内排列多个卡片还能根据不同屏幕尺寸动态调整每行列数和卡片大小。数据绑定上核心是TaskViewHolder通过onBindViewHolder把任务信息绑定到卡片视图上。大致逻辑是Override public void onBindViewHolder(TaskViewHolder holder, int position) { TaskViewModel.Task task mLauncherTasks.get(position); holder.bindTask(task, mIconDrawable); }卡片的图标、标题、背景色都是在这里设置的。值得注意的是卡片背景色的深浅和壁纸联动CarLauncher 会读取壁纸亮度来调整卡片底色这个会单独讲。如果你要自定义车载桌面GridCardView 是性价比最高的修改点。比如调整卡片圆角、卡片间距、每行展示数量甚至改成横向卡片流都可以在这里动手。2.4 Workspace快捷方式页面的展示与交互Workspace 在 CarLauncher 里承担的是传统桌面“桌面页”的角色存放固定快捷方式。它继承自PagedView支持左右滑动切换页面每一页摆放若干快捷方式。源码实现里Workspace 的页面数据来源和 TaskView 不同。TaskView 是实时从系统任务栈拉取的Workspace 则通过一个桌面存储服务来读写快捷方式数据。这里要留意的是CarLauncher 的 Workspace 并不像手机 Launcher 那样支持长按拖拽摆放该功能在车载场景中优先级不高所以默认没有实现。Workspace 与 TaskView 的布局层级也有讲究。在默认布局中TaskView 在上部Workspace 在下部两个区域的滚动逻辑是独立的。TaskView 区域内的卡片可以横向滚动Workspace 整个页面也可以横向滑动这种嵌套滑动是定制时最让人头疼的部分后面我会专门讲冲突怎么解决。2.5 壁纸管理与状态栏协调车载桌面和手机一样支持动态壁纸和静态壁纸但 CarLauncher 对壁纸的处理多了一层系统协调。在onCreate里CarLauncher 会注册一个WallpaperManager.OnWallpaperChangedListener壁纸变化时会调用mWorkspace.onWallpaperChanged()来刷新壁纸。这里有个提识别的小技巧CarLauncher 计算壁纸是否适合浅色还是深色文字主要依赖WallpaperManager中的异步壁纸提取。如果壁纸整体偏亮TaskView 卡片和文字就会自动切成深色反之则保持浅色这是为了防止桌面内容和壁纸混在一起看不清。实际开发时我们会用setWallpaperDimAmount来控制壁纸压暗程度让前景内容更突出mWallpaperManager.setWallpaperDimAmount(getResources().getFloat(R.dimen.dim_amt));压暗比例一般设置在 0.2 到 0.35 之间。调太大壁纸就完全变黑了调太小又不够突出前景卡片。3. 自定义车载 Launcher 开发实操3.1 环境准备AOSP 源码编译加 Android Studio 双轨方案做自定义车载 Launcher我建议走“双轨方案”源码编译验证和 Android Studio 日常开发并行。第一轨是 AOSP 源码环境。拿到packages/apps/Car/Launcher源码后我一般是先在改过的源码树上编译整个模块验证逻辑是否存在问题命令大致是source build/envsetup.sh lunch 你的产品名 mmm packages/apps/Car/Launcher编译产物会出现在out/target/product/目标平台/system/priv-app/Launcher/目录下。系统级应用一般都放在priv-app而不是普通app目录因为车载桌面需要privileged权限才能访问系统任务和部分系统 API。第二轨是独立工程。把 Launcher 源码完整拷贝到 Android Studio 工程里替换包名和 ApplicationId然后引入框架依赖。这里有一个必须处理的坑CarLauncher 依赖android.car这些系统库在普通 SDK 里是没有的你需要从源码编译产物里找到framework-car.jar放入工程的 libs 目录并在 Gradle 配置里标注compileOnly避免打进 APKcompileOnly files(libs/framework-car.jar) compileOnly files(libs/framework.jar)如果你只是做定制验证而不是交付系统直接用系统签名对 APK 签名后 push 到/system/priv-app/重启即可不需要每次重新编译完整系统镜像。这个流程能极大提速。需要提醒的是车载桌面这种系统应用不能直接在 Android Studio 里 run一个是签名问题一个是系统权限问题另一个是因为它作为系统HOME应用直接运行时系统会把它当普通应用拉起来但缺少 HOME 应用所需的系统 API 权限很多功能会抛异常。建议的做法是打系统签名后再安装安装到系统分区验证。3.2 继承 TaskView 打造属于自己的桌面组件搞懂源码之后我第一次实际动手做的就是不想改 AOSP 原工程而是在自己工程里继承 TaskView 来做定制。这样既能保留原生逻辑又能灵活控制视图。首先定义一个自己的 TaskView 子类public class CustomTaskView extends TaskView { public CustomTaskView(Context context, AttributeSet attrs) { super(context, attrs); } Override protected void onFinishInflate() { super.onFinishInflate(); // 在这里替换卡片子布局、调整 GridCardView 参数 } }这里要注意一点TaskView 构造之后会创建TaskViewController但真正开始加载数据是在 Launcher 主 Activity 里调用taskViewController.start()之后。如果你继承了 TaskView不要直接在构造函数里强行拉数据而是要确保 Launcher 对 controller 有一套完整的 start/stop 生命周期管理。如果你想把原生 TaskView 里的 GraidCardView 布局换掉比如从网格改成横向一排可以替换res/layout/grid_card_view.xml的布局或者直接修改TaskView里的 inflate 逻辑。我建议把整个任务卡片区作为独立控件模块化内部用 RecyclerView 实现外部只暴露数据接口这样后续维护会轻松很多。3.3 自定义 Workspace 布局与动态调整卡片参数Workspace 的自定义主要集中在这几块每页行数、列数、图标尺寸、边距间距。这些参数在 CarLauncher 里分散在多个资源文件里定制时最好统一抽取到一个 dimens 或 config 类里。以我的项目为例目标屏幕是 1280x800 横屏DPI 是 160我在资源文件里做了这样的参数调整dimen nameworkspace_cell_width80dp/dimen dimen nameworkspace_cell_height88dp/dimen integer nameworkspace_num_columns7/integer integer nameworkspace_num_rows1/integer这里有一个计算逻辑屏幕可用宽度除以每列数再减去间距就能得到图标区域宽度。如果图标间距过大看起来会很散间距过小用户误触率会上升。我经验值是横向边距不小于 12dp卡片之间的留白在 20dp 到 32dp 之间比较合适。Workspace 默认是不支持长按拖动的但很多车厂会要求这个功能。如果你也要实现建议直接参考 AOSP 中 Launcher3 的DragController相关代码移植过去不要在 CarLauncher 现有代码上硬改因为 CarLauncher 的数据存储机制和 Launcher3 差异太大硬改会把模块耦合死。3.4 Manifest 配置与系统集成要点自定义桌面要能作为系统的 HOME 启动Manifest 里必须有这么一段activity android:name.CustomCarLauncher android:launchModesingleTask android:exportedtrue android:stateNotNeededtrue android:windowSoftInputModeadjustPan|adjustNothing android:themestyle/Theme.CarLauncher intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.HOME / category android:nameandroid.intent.category.DEFAULT / /intent-filter /activity这里有三个关键点。第一launchMode必须设置为singleTask否则用户按 HOME 键回到桌面时可能出现多个桌面实例内存和焦点都会乱。第二exported要设为true因为系统需要从外部启动它。第三最好加上android:stateNotNeeded因为系统重启或回收时会重建 Launcher这个属性可以避免不必要的状态保存流程。系统权限方面去读任务栈需要设置权限但不是放在 Manifest 里而是依靠系统签名。所以 Android 11 上做定制桌面一定有一道签名关卡绕不过去。3.5 性能优化的几个关键点车载桌面对启动速度和交互响应非常敏感我做了几项针对性优化。冷启动速度方面CarLauncher 的onCreate里不要做过多耗时操作。应用列表和任务列表的加载可以异步化先渲染空布局再逐步填充数据。我实测中把工作区数据和 TaskView 数据拆开加载后冷启动时间大约缩短了 300ms。列表复用上GridCardView 已经用了 RecyclerView这个不用操心但卡片 item 本身不要用太复杂的布局层级。原生的 task_card 布局里有不少嵌套我当时直接改成了一个根布局加两个子控件的结构滚动流畅度明显提升。还有一个容易忽略的问题是内存泄漏。CarLauncher 作为常驻系统 HOME一旦泄漏就是常驻泄漏后果非常严重。特别注意两点TaskViewController注册了系统任务监听必须在onDestroy里反注册壁纸监听也要在销毁时移除否则几十兆内存就白白挂在那里。Override protected void onDestroy() { mTaskViewController.stop(); mWallpaperManager.removeOnWallpaperChangedListener(...); super.onDestroy(); }4. 常见问题与排查技巧实录4.1 编译报错TaskView 找不到或方法不存在第一个高频问题是把 CarLauncher 抽到 Android Studio 后编译直接报TaskView找不到或方法签名对不上。这大概率是系统依赖没配好或版本偏差。Android 11 的 CarLauncher 里很多依赖是 AOSP 独有接口普通 Android SDK 里没有不看日志根本猜不到。我的排查方式是先看import段凡是来自com.android.car或com.android.internal的类都确认一下 framework-car.jar 是否在 compileOnly 路径里。然后确认方法签名用javap查看系统 jar 里对应类的方法列表避免拿 Android 12 的代码去适配 Android 11 的 jar。4.2 滑动冲突RecyclerView 和 Workspace 的嵌套滑动车载桌面里 TaskView 下方是 WorkspaceWorkspace 是可以横向滑动的 PagedViewTaskView 里的 GridCardView 也可以横向滑动。两个横向滑动容器叠在一起几乎必然会出现手势冲突。我踩到的情况是任务卡片滚动到边缘时继续滑动会把整个 Workspace 页面也带走手势非常“黏”。处理方法有两个方向。一是给 GridCardView 设置requestDisallowInterceptTouchEvent来控制 Workspace 的拦截横向手势在子容器开始滑动时通知父容器不要拦截。二是自己重写 Workspace 的onInterceptTouchEvent判断如果触点落在 TaskView 区域就不参与滑动拦截。我当时选择的是第二种因为车载场景下 TaskView 区域和 Workspace 区域的边界固定直接在 Workspace 拦截逻辑里根据触摸起始位置判断是否交给父容器处理逻辑最简单也最稳定。4.3 焦点与按键导航问题车载场景除了触屏还有旋钮和方向盘按键这些输入本质是按键事件需要走焦点机制而不是触摸事件。默认的 CarLauncher 里焦点跳转顺序不一定符合车厂要求常见问题是“焦点找不到”或“焦点跳到壁纸上”导致按键无响应。解决办法是在自定义视图里手动指定焦点搜索策略。比如给每个任务卡片设置android:nextFocusLeft、nextFocusRight属性或者重写getDefaultFocus让第一个卡片在桌面打开时自动获取焦点Override public View getDefaultFocus(View focused, int direction) { return mTaskView.getChildAt(0); }这里还有一个经验点音量键和媒体键在车载桌面上不能只靠 Activity 的 onKeyDown 处理很多按键事件会从 SystemUI 或 CarService 层分发桌面应用要处理的话最好通过CarInputService或者系统按键广播做统一转发不要在 Activity 内硬编码按键值。4.4 壁纸不显示或显示错位壁纸显示异常是另一个高频问题。CarLauncher 的壁纸逻辑是和 SystemUI 的壁纸窗口联动的如果单独调试应用而不走完整系统启动流程壁纸经常是空白。这里有个常用做法在测试时先设置一张静态壁纸通过adb shell wallpaperset或者开发者选项里调整确认壁纸取色、压暗逻辑正常再做动态壁纸适配。如果壁纸错位多半是WallpaperManager的壁纸偏移没有同步到自己的 Workspace。CarLauncher 里是通过监听壁纸偏移接口来同步的定制时不要丢掉这个监听逻辑mWallpaperManager.getIWallpaperManager().setWallpaperOffsetSteps(...);4.5 Android 11 分区存储带来的文件访问限制做车载桌面时经常要访问一些本地文件比如皮肤资源、主题包。Android 11 开始强制分区存储应用不能随意读取其他应用的私有目录和公共目录的任意路径。很多开发者在网上搜到类似content://....Android/data/这样的路径以为能直接访问其实是被系统拦掉的。车载桌面作为系统应用处理这类需求的正规做法是用StorageManager申请所有存储权限或者走MediaStore来读写公共媒体文件。如果必须访问某个特定应用的私有目录需要对方应用通过FileProvider显式授权不能硬闯Android/data目录。这个限制在 Android 11 之后越收越紧车厂定制时最好提前规范好文件访问方案不要等集成阶段再补。4.6 常见问题速查表现象常见原因排查/解决办法桌面无法作为默认 HOMEManifest 缺少 HOME category检查 intent-filter确认包含 MAIN/HOME/DEFAULT按 HOME 键出现多个桌面launchMode 不是 singleTask在 manifest 设置 singleTask点击任务卡片重启应用使用的是 startActivity 而非 moveTaskToFront改为通过 taskId 调用 moveTaskToFrontRecyclerView 滑动触发父页面滚动嵌套滑动冲突在 Workspace 触摸拦截中按区域判断或使用 requestDisallowInterceptTouchEvent遥控器按键无反应焦点不在目标视图上手动设置默认焦点和 nextFocus 方向壁纸不跟随桌面壁纸监听未注册或偏移逻辑缺失注册 WallpaperManager 监听并同步壁纸偏移编译找不到 android.car 类缺少 framework-car.jar编译产物中引入 framework-car.jar 并 compileOnly安装后停止运行未使用系统签名或 priv-app 权限不足使用平台签名重新签名放到 priv-app 目录卡片图标不更新应用信息缓存未刷新在 onResume 中刷新 TaskViewModel这套排查表基本覆盖了我在实际项目中碰到的绝大多数问题。如果你做的车机对自己定制的 Launcher 有更强的交互诉求比如双屏联动、分屏快捷入口原理上都是通过修改 TaskView 和 Workspace 的数据源以及点击策略来完成逻辑是相通的。最后说一点个人体会做车载桌面开发最忌讳的就是只盯着 UI 层改不看系统层机制。CarLauncher 的知识体系横跨 Activity 启动模式、任务栈管理、焦点分发、壁纸服务、分区存储等多个系统模块只有把它们串起来理解才能做出稳定、流畅的车载桌面。如果你想入门建议先按我上面说的方式把原生源码跑起来再从 TaskView 这个点切开逐步往周边扩展这样比硬啃整棵树要高效得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门