mcpField
I
iphone-duo-flutter

iphone-duo-flutter

github.com/mirzaaghazadeh开发250472 次安装今天更新
代码仓库

关于 iphone-duo-flutter

面向 Flutter 开发者的 iPhone Duo 适配指南,介绍用约束和独立安全区构建自适应布局,并说明如何通过平台通道接入折叠区域等原生信息。

主要功能

  1. 01用约束处理窗口尺寸变化
  2. 02分别处理各边安全区内边距
  3. 03按宽度断点切换导航布局
  4. 04通过平台通道查询区域并接收铰链事件

适用场景

  1. 01修复开合或分屏时的布局问题
  2. 02为内外屏切换导航栏与导航轨
  3. 03让交互控件避开折叠与摄像头区域

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mirzaaghazadeh/iphone-duo-skills --skill iphone-duo-flutter

安装后在 Claude Code 中输入/iphone-duo-flutter确认已加载。

详细介绍

iPhone Duo 上的 Flutter

请从这里开始,否则这件事会浪费你一下午的时间:

MediaQuery.displayFeatures 的文档说明它“仅在 Android 上填充”。

Flutter 的折叠屏抽象是为 Android 铰链设计的。在 iPhone Duo 上,无论设备折叠到什么程度,它都会返回空列表。dual_screen 软件包(TwoPane、hingeAngleEvents)也一样——实际上仅适用于 Android,且上次发布已是大约三年前。

因此,Flutter 开箱后无法感知这台设备的折叠状态。不过,它确实提供了大部分真正重要的功能。分两个层级处理。

第 1 层——现在就能用,无需原生代码

优秀移植工作的主体都在这一层,而且完全使用 Dart 实现。

正确处理尺寸变化

iPhone Duo 会不断改变应用的尺寸:打开、合上、折叠、旋转以及进入 Split View。Flutter 会处理这些变化,但前提是布局由约束驱动,而不是由固定值驱动。

  • 使用 LayoutBuilder 及其 BoxConstraints 做布局决策。
  • 需要窗口尺寸时,使用 MediaQuery.sizeOf(context),而不是 MediaQuery.of(context)——sizeOf/paddingOf 访问器只会在所读取的属性变化时触发重建,这一点在尺寸如此频繁变化时很重要。
  • 绝不要在 initState 或字段中缓存尺寸。几秒内它就会过时。
处理不对称安全区域

这是会导致实际故障的问题。在 iPhone Duo 上,系统控件位于一侧,因此内边距通常是不对称的——具体位于哪一侧,取决于设备姿态以及你占据 Split View 的哪一半。

MediaQuery.paddingOf(context) 会返回 EdgeInsets,其中 left、 top、right、bottom 各自独立。请分别使用。任何类似 width - padding.left * 2 的表达式,在这台设备上都是缺陷。

SafeArea 微件已经能独立处理四条边,因此应优先使用它,而不是手动编写内边距。如果需要让插画延伸到安全区域下方,请将背景放在 SafeArea 外,只将可交互的前景放在其中。

用尺寸类别思路取代断点

iOS 使用尺寸类别来处理这台设备:外屏是紧凑宽度,内屏是常规宽度。Flutter 没有尺寸类别,因此可以映射到宽度断点——Material 3 窗口尺寸类别是合理的参考(紧凑 < 600、中等 600–840、扩展 ≥ 840 逻辑像素)。

两条规则:

  • 根据传入的约束进行分支,而不是根据 Platform.isIOS、设备型号字符串或 MediaQuery.orientationOf。内屏不遵循受支持的方向设置,因此方向不是可靠的信号。
  • 根据你的内容何时无法正常显示来选择断点,然后检查紧凑/常规的划分是否恰好适用。
优先使用自适应容器

宽布局使用 NavigationRail,窄布局使用 NavigationBar,这相当于 Flutter 实现了 iOS 原生将控件移到侧边的行为。内屏使用侧边栏、外屏使用导航栏,效果会比较贴合,而且只需一个断点。

第 2 层——需要平台通道

所有真正针对 iPhone Duo 的功能,在桥接之前 Dart 都无法使用。截至本文撰写时,还没有 Flutter 版本发布对 iPhone Duo 的支持,因此请做好自行编写的准备,并在动手前确认该支持是否已加入。

值得桥接的功能,按优先级排序如下:

原生 API(iOS 27.1)在 Dart 中使用的原因
UIView 上的 reservedRegions(kind:)避免控件落在折叠处或屏下摄像头区域
UIHingeInteraction获取连续的铰链角度,用于实现效果
traitCollection 中的尺寸类别获取 iOS 使用的真实信号,而不是依赖你猜测的断点
如何设计桥接

对于一次性查询(当前保留区域、当前尺寸类别),使用 MethodChannel;对于任何持续变化的数据,使用 EventChannel——铰链角度是实时数据流,用 MethodChannel 会产生大量调用。

在 iOS 端的 Runner 目标中,在 FlutterViewController 上注册 FlutterMethodChannel 和 FlutterEventChannel,然后从该视图读取原生 API。保留区域 API 属于 UIView,而 FlutterViewController 的视图正是要查询的正确视图——Dart 布局实际占据的就是这个视图。

将区域序列化为普通映射({left, top, width, height, kind, active}),这样 Dart 就能据此重建 Rect。区域发生变化时发送区域列表,而不是轮询。

所有操作都要做好防护:在所有非折叠设备上,铰链值都是 nil;而且这些 API 在 iOS 27.1 以下不存在。桥接必须在遇到这些情况时退化为“无区域、无铰链”,而不是抛出异常,因为同一个 Flutter 二进制文件会运行在每一台 iPhone 上。

然后像 Tier 1 那样使用

区域到达 Dart 后,将其传入你已经构建好的同一套约束驱动布局。分隔区域(折叠处)会告诉你哪些位置不要放置交互式组件;遮挡区域(摄像头)会告诉你要避免遮挡哪些位置。通常,使用 Stack 搭配 Positioned,或自定义 MultiChildLayoutDelegate,是让特定控件避开区域的最简洁方式。

按以下顺序操作

  1. 让应用在 LayoutBuilder 下能够干净地调整大小。大多数 Bug 都能在这里解决。
  2. 检查每一处使用 MediaQuery.padding 的代码,留意是否进行了对称内边距计算。
  3. 用基于约束的分支逻辑替代方向和平台检查。
  4. 增加分栏布局,让内屏不只是拉伸后的手机布局。
  5. 只有在应用确实需要折叠感知时,才编写通道。

步骤 1–4 能让 Flutter 应用在 iPhone Duo 上的大部分体验都达到理想状态,同时也能改善它在平板电脑和桌面设备上的表现。步骤 5 是唯一专为 iPhone Duo 设计的部分,也是唯一无法在没有 Xcode 27.1 模拟器的情况下测试的部分。

要了解原生 API 的实际功能及其原因,请阅读 ../iphone-duo-adaptive-layout/SKILL.md 和 ../iphone-duo-readiness/SKILL.md——尽管代码不能直接迁移,设计思路可以直接借鉴。

评论

支持 Markdown