☰
DiPlay 侧边面板实现解析:让 CarPlay 与车载信息分屏共享主屏幕
2026/10/9 7:32:54 网站建设 项目流程
  • 移动开发
  • 智能硬件
  • 音视频

【免费下载链接】DiPlay

Independent CarPlay receiver for compatible Android head units. Wired and wireless public preview.

项目地址:https://gitcode.com/gh_mirrors/di/DiPlay
点击查看免费下载

DiPlay 的「CarPlay 旁侧边面板」(Side panel,实验性功能)让 iPhone 把 CarPlay 画在主屏幕的三分之二,而 DiPlay 在剩下的三分之一里显示自己的 Android 面板——横屏时位于乘客一侧,竖屏时位于屏幕底部,且随时可切回全屏 CarPlay 而无需重连。本文以仓库文档 docs/SIDE_PANEL.md 为主线,结合 CarPlayViewAreas.kt 与 CarPlayHostActivity.kt 的源码实现,讲清这一功能的设置方式、view area 原理、面板的布局与刷新机制,以及同一轮研究中验证的 iAP2 协议边界。读完你可以掌握:如何在 DiPlay 中开启并切换该功能、它依赖的updateViewArea调用链,以及 CarPlay 流上叠放自定义 Android 视图的通用做法。

功能概览与开启方式

功能的行为在文档中描述得很直接:CarPlay 与 DiPlay 共享车机主屏,iPhone 在屏幕的三分之二内绘制 CarPlay,DiPlay 在剩余区域显示自己的面板。具体布局规则为:

  • 横屏:CarPlay 保持位于驾驶员一侧,面板占据乘客一侧的三分之一——左舵车在右侧,开启Right-hand drive(右舵)后换到左侧;
  • 竖屏:面板是屏幕底部的一条横幅(band);
  • 车机可以随时在这两种形态(面板/全屏 CarPlay)之间切换,无需重连;
  • 功能默认关闭,入口在Settings → Display and performance → Side panel next to CarPlay;
  • 开启后,会话内的菜单会出现Side panel按钮,面板自身则有一个Full screen按钮用于切回全屏。

从字符串资源 strings.xml 可以看到该功能的官方描述(含"实验性"标注与"下次连接时生效"的适用前提):

<string name="side_panel">Side panel next to CarPlay (experimental)</string> <string name="side_panel_description">CarPlay can move to two thirds of the screen while DiPlay shows a panel in the rest: on the passenger's side of a landscape screen, at the bottom of a portrait one (clock and, where it is read, the battery). Show and hide it from the in-session menu. Applies at the next connection.</string> <string name="side_panel_show">Side panel</string> <string name="side_panel_full_screen">⤢ Full screen</string>

注意字符串中两个关键前提:面板显示时钟,以及"已读取到时的电量"(battery 一项依赖车机侧数据能否读到);设置变更在下一次连接时生效。对应的持久化实现在 SidePanel.kt:

object SidePanelSettings { private const val PREFS = "diplay_side_panel" private const val KEY_ENABLED = "enabled" fun enabled(context: Context): Boolean = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(KEY_ENABLED, false) fun setEnabled(context: Context, enabled: Boolean) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(KEY_ENABLED, enabled).apply() }

即独立的 SharedPreferences 文件diplay_side_panel,键enabled,默认值false。会话建立时该开关被传入 view area 的构建参数(CarPlayHostActivity.kt 第 3529–3541 行 中sidePanel = SidePanelSettings.enabled(this)),因此关闭状态下本次会话根本不会声明侧边面板区域,也就没有对应的菜单按钮——这解释了"下次连接生效"的说法。

工作原理:基于 View Areas 的面积声明与切换

该功能构建在 DiPlay 的 view areas 机制之上(详见 docs/VIEW_AREAS.md)。CarPlay accessory 可以在主屏视频流里声明多个 view area,并告诉 iPhone 当前画在哪一个。开启侧边面板后,主屏在每种屏幕方向、每个 dock 边缘下额外声明一个占屏幕三分之二大小的区域:横屏取左侧(或右舵时的右侧),竖屏取顶部。

源码侧的核心是 CarPlayViewAreas.kt。其中区域按用途分成三类,SIDE_PANEL只在用户请求时才会被选中:

/** What an area is for; SIDE_PANEL is CarPlay beside DiPlay's panel and is only chosen on request. */ enum class Kind { FULL_SCREEN, SPLIT_SCREEN, SIDE_PANEL }

build()构造侧边面板区域时(第 148–158 行):

if (sidePanel) for (screen in screens) { // CarPlay keeps two thirds: the driver's side of a landscape screen (the panel on the // passenger's side) or the top of a portrait one (the panel at the bottom). val width = if (screen.portrait) screen.width else (screen.width * 2 / 3) and 1.inv() val height = if (screen.portrait) (screen.height * 2 / 3) and 1.inv() else screen.height val originX = if (rightHandDrive) screen.width - width else 0 for (edge in edges) { areas += AirPlayViewArea(width, height, originX = originX, dockEdge = edge) slots += Slot(Kind.SIDE_PANEL, screen.portrait) } }

要点:横屏面积为屏幕宽的2/3(向下取整到偶数,and 1.inv()),竖屏为高的2/3;originX由rightHandDrive决定,实现左舵/右舵镜像。index()/sidePanel()负责按"当前方向 + dock 边缘"查回对应区域索引,use()记录当前使用的区域,整个类不依赖任何 Android 类型(除偏好设置外),因此可完整单元测试(见 CarPlayViewAreasTest.kt)。

切换动作走 CarPlay 事件通道的updateViewArea。AirPlayInfoPlist.kt 第 170 行附近 记录了在 Tang 车 + iOS 27 上实测到的消息形态:iPhone 只有在带上了动画时长(animationDurationMillis)和相邻区域(adjacentViewAreas)时才响应切换。消息最终经 AirPlaySession.showViewArea() 发出,编排层 CarPlayController.showViewArea() 透传。

关键在于"iPhone 只知道自己被要求用更小的区域",对面板内容一无所知。因此 DiPlay 采取的做法是:

  • 画布按整块屏幕布局:layoutArea()对SIDE_PANEL类区域返回对应的全屏区域(第 63–66 行),保证流内容不变、iPhone 继续发送整帧;
  • 普通 Android 视图盖住黑条:panelRect()计算 CarPlay 留下的黑色条带(流坐标),第 72–84 行 分别处理横屏左右侧与竖屏底部三种情况;
  • 触摸分区:面板内触摸留在 DiPlay,其余区域照旧交给 iPhone;
  • 窗口变化收场:屏幕转向、车机分屏等窗口变化会结束面板并按常规选择匹配区域;关闭 DiPlay 或会话结束则隐藏面板并停止其刷新。

面板的构建、定位与刷新(源码走读)

面板内容:时钟、电量与全屏按钮

CarPlayHostActivity.kt 的buildSidePanel()(第 1510–1539 行) 构造了一个垂直LinearLayout,包含三部分:

  1. 一个TextClock(24 小时制HH:mm/ 12 小时制h:mm,72sp 白字);
  2. 一个电量TextView(sidePanelBattery,30sp);
  3. 一个Full screen按钮,点击调用showSidePanel(false)切回全屏。

面板背景为深色(rgb(16,16,18)),且isClickable = true——这使落在面板上的触摸被其消费,不再穿透到下方的手势层/CarPlay 流。

定位:把流坐标映射到视频布局

iPhone 持续发送整帧,而视频视图在窗口内可能被 letterbox(黑边缩放)。placeSidePanel()(第 1542–1557 行)把面板条带从流像素坐标换算到窗口坐标:

private fun placeSidePanel(viewWidth: Int, viewHeight: Int) { val panel = sidePanel ?: return val display = sessionDisplay ?: return val strip = display.viewAreas?.let { it.panelRect(it.current) } ?: return val content = contentRect(viewWidth, viewHeight) val scaleX = content.width / display.width val scaleY = content.height / display.height val left = Math.round(content.left + strip.originX * scaleX) val top = Math.round(content.top + strip.originY * scaleY) panel.layoutParams = FrameLayout.LayoutParams(...).apply { // Stream coordinates are physical pixels, independent of the UI's reading direction. leftMargin = left; topMargin = top // Gravity.TOP or Gravity.LEFT } }

两个值得注意的设计:

  • letterbox 跟随:contentRect是视频内容在窗口内的实际矩形,因此面板始终精确盖在流的黑条上,而不是简单地贴窗口边缘;
  • 物理坐标不受 RTL 影响:Gravity.TOP or Gravity.LEFT+ 流像素坐标,保证界面切到右起书写语言(如阿拉伯语、希伯来语)时面板不会翻到 CarPlay 上面去。这一点由测试 SidePanelLayoutTest.letterboxedPanelKeepsItsPhysicalPassengerSideInEitherLayoutDirection 覆盖:2560×1440 流放入 1200×1200 窗口(正方形 letterbox),在 LTR/RTL × 左舵/右舵四种组合下,面板left恒为 800(左舵)或 0(右舵),且尺寸一致(400×675,top=263)。另一个测试portraitPanelCoversTheBottomStripOfTheSquareStream验证了竖屏(旋屏方形流)下面板覆盖底部条带。

显示、隐藏与状态恢复

showSidePanel(show)(第 1559–1574 行)是切换入口:

  • 显示时取areas.sidePanel(portrait),隐藏时取areas.indexFor(...)(当前窗口形状匹配的全屏/分屏区域);
  • 通过controller?.showViewArea(target)下发updateViewArea,只有下发成功才更新状态(sent为 false 时直接返回),避免 UI 状态与实际区域不一致;
  • 记录日志Side panel shown/hidden: view area $target sent=$sent;
  • 显示时启动周期刷新sidePanelTick,隐藏/重置时移除回调;
  • 最后updateVideoLayout(...)让画布按整屏重新布局。

会话菜单中的按钮同样以区域是否存在为前提——sessionDisplay?.viewAreas?.sidePanel() != null时才渲染Side panel / Full screen切换按钮(第 1612–1621 行)。

状态收场时机:源码中多处调用resetSidePanel()——新会话开始时"不带侧边面板启动"(第 4060 行)、屏幕转向或进入车机分屏时"结束面板"(第 4246 行)。窗口大小变化时若面板仍显示则重新placeSidePanel(第 4327 行)。

电量刷新的条件:refreshSidePanel()(第 1582–1588 行)只在 DiPlay 已为 iPhone 读取电量的场景(BydOutputSettings.batteryToIphoneActive)下才显示"🔋 xx% · xx km",否则留空——即字符串资源中"where it is read"的实现。刷新周期为常量SIDE_PANEL_REFRESH_MILLIS = 5_000L(第 4941 行),由sidePanelTick在主线程上自调度,面板关闭即停止。

会话接管(adoption):当新的宿主 Activity 接管一个面板已打开的存活会话时,adoptBackgroundSession会恢复面板视图与刷新回调,且不改变 view area(区域索引current在CarPlayViewAreas内跨 Activity 共享)。这一行为由 SidePanelLayoutTest.adoptingAnOpenSidePanelRestoresItsViewAndRefreshWithoutChangingTheArea 验证:接管后areas.current仍指向侧边面板区域,sidePanelShown为 true、面板可见、sidePanelTick已挂入 Handler。生命周期另有 SidePanelLifecycleTest.kt 覆盖。

实测结果

文档记录的实测环境:2024 款 BYD Tang(DiLink 5.0,2560×1440 可旋转中控屏)+ iOS 27 iPhone,无线 CarPlay(同设计的实验室构建):

  • CarPlay 在横屏(左侧 2/3)与竖屏(顶部 2/3)之间移动并切回,耗时不到一秒,全程无需重连;
  • CarPlay 区域内的触摸全部正确命中;
  • 面板显示时钟与车辆电量和续航里程;
  • 一个 dock 相关发现:声明了多个区域后,CarPlay 在 Automatic dock 下会把 dock 放到屏幕底部;需要通过 dock 设置来指定位置(与 docs/VIEW_AREAS.md 中"声明第二个区域后 Automatic dock 停靠底部"的结论一致)。

演进方向:从投屏到 CarPlay 外围的启动器

由于面板就是一个普通 Android 视图,它能显示 DiPlay 可读取或操作的任何东西。文档列出的方向包括:

  • 经车机 adb 读取的车辆数据:空调、胎压、车门状态;
  • 对车辆的控制(例如 BYD autoservice 中已记录的空调 fid);
  • 小组件、BYD 应用快捷方式,或其他地图源的导航图。

再补上几块拼图——方向盘按键、驻车时自动显示面板的规则、按屏幕尺寸区分的布局、面板侧边选择——DiPlay 就能作为 CarPlay 外围的一个"launcher"而不仅仅是一块全屏投屏。这一方向与仓库中已存在的车辆数据桥接模块(如 BydNavigationOutputs.kt、BydHudBridge.kt 等 hud 包实现)在数据链路上是衔接的:面板当前的电量读数正是复用了为 iPhone 供电量的同一数据源。

同一轮研究的其他 iAP2 发现

在同一台车、同一实验室构建上,参考 Apple iAP2 消息规范(Xcode CarPlay Simulator 的iap2messages-internal.i2mspecarchive,仅作为数据读取),还验证了以下边界:

  • VehicleStatus 附加项:在 VehicleStatus 组件中声明OutsideTemperature、InsideTemperature、WiperStatus、BarometricPressure、Alerts与PassengerSeatStatus均被接受;iPhone 随后订阅了车外温度、雨刮、气压、警报与乘客座椅状态(唯独没有车内温度)。从车端发送实时值(车外温度、雨刮、双闪)也被接受,但 CarPlay 与 Apple Maps 界面上没有任何可见变化;
  • 道路物体检测(identification 参数 33,供车辆摄像头报告路牌、车道与物体):iPhone 拒绝了该 identification(对 0x0021 返回IdentificationRejected),推测保留给经过认证的车辆;
  • 应用发现(对所有 CarPlay 应用发起带图标的StartAppDiscoveryUpdates):iPhone 以AppDiscoveryUpdate应答,CarPlayAppListAvailable = 0(Unknown)且没有列表。

这些结果说明:CarPlay 侧对 accessory 的声明是"选择性信任"的——接受更多声明不等于这些能力会在 UI 中兑现,而部分能力(如道路物体检测)则存在明确的准入限制。理解这一点,有助于正确评估类似"车端数据注入 CarPlay"这类功能的实际边界。

小结

DiPlay 的侧边面板是一个以"最小改动撬动最大效果"为特点的实验性功能:它没有发明新的传输能力,而是复用 CarPlay 既有的多 view area 声明 +updateViewArea无重连切换机制,再用一个定位精确(流坐标 → 视频布局映射)、触摸隔离、5 秒周期刷新的 Android 视图填满 CarPlay 让出的黑色条带。设置入口在 Display and performance 页,默认关闭、下次连接生效;核心逻辑集中在 CarPlayViewAreas.kt(区域计算)、CarPlayHostActivity.kt(面板构建/定位/刷新/收场)与 SidePanel.kt(开关持久化),并有 SidePanelLayoutTest.kt、SidePanelLifecycleTest.kt、CarPlayViewAreasTest.kt 三组测试对定位数学、RTL 稳定性、会话接管与区域选择行为提供验证。若要在此基础上扩展面板内容(车辆数据、控制、小组件),面板就是一个普通的 Android 视图树,可任意替换buildSidePanel()的构建逻辑,而 view area 侧完全不用改动。

  • 移动开发
  • 智能硬件
  • 音视频

【免费下载链接】DiPlay

Independent CarPlay receiver for compatible Android head units. Wired and wireless public preview.

项目地址:https://gitcode.com/gh_mirrors/di/DiPlay
点击查看免费下载

相关推荐

上一篇:Velox扩展开发教程:如何自定义高性能数据处理组件
下一篇:10分钟上手openeuler/libvirt:从安装到创建虚拟机的快速教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询