Flutter 获取 iPhone Duo 预留区位置

摘要

tw_iphone_duo_reserved_region 将 iOS 当前激活的遮挡区域转换为 Flutter 窗口逻辑坐标中的 Rect。需要为自定义界面避开摄像头等局部遮挡时,可以读取这些矩形并自行调整布局。本文先说明如何判断是否需要插件、如何接入及处理返回值,再介绍实现与验证方式。

什么是 iPhone Duo Reserved Region

Reserved Region 是视图坐标空间中被系统实体占用的区域。它有两种与布局相关的类型:division 将可用空间分割成多个区域,例如 iPhone Duo 的折痕;occlusion 只遮挡局部内容,例如启用中的 FaceTime 摄像头。本插件只返回当前激活的 occlusion 区域 ,不会返回折痕对应的 division 区域。本插件也不提供设备姿态或铰链状态信息。

SafeArea 与 Reserved Region 怎么选

SafeArea Reserved Region
表达形式 从窗口边缘向内留出安全空间。 位于视图坐标空间的具体区域;本插件提供激活的遮挡矩形。
适用场景 常规页面、导航栏、底部操作区等需要避开系统边缘内容的布局。 自定义边缘 UI、悬浮按钮等需要判断是否与局部遮挡相交的布局。
布局方式 由系统和 Flutter 的标准组件或 SafeArea 处理边缘留白。 将组件边界与遮挡 Rect 比较,再按业务需要移动或重排。

标准组件优先依赖系统适配;只有自定义边缘 UI 或需要精确避让时,才读取 Reserved Region。插件提供几何信息,不会自动修改 padding 或组件位置。

使用前提

项目 要求
Dart >=3.10.0 <4.0.0
Flutter 3.44 或更高版本
iOS Deployment Target 15.5 或更高版本
编译环境 Xcode 27.1 或更高版本,包含 iOS 27.1 SDK
Reserved Region 运行时 iOS 27.1 或更高版本的受支持设备

最低部署版本与 API 可用版本是两个不同概念。应用可以部署到 iOS 15.5,但编译必须使用 声明了 Reserved Region API 的 SDK;在较早运行时上,原生层通过可用性检查返回 null。

推荐接入方式

在应用根部安装 Bridge,让后代组件通过 Provider 读取最新结果:

dart 复制代码
TwIphoneDuoReservedRegionBridge(
  child: MaterialApp(
    home: const HomePage(),
  ),
)

在需要避让的组件中读取:

dart 复制代码
final List<Rect>? regions =
    TwIphoneDuoReservedRegionProvider.activeOcclusionRegionsOf(context);

只有应用已自行管理刷新、缓存和状态分发时,才需要直接查询 Channel:

dart 复制代码
final List<Rect>? regions =
    await const TwIphoneDuoReservedRegionChannel().load();

返回值与坐标语义

返回值 含义 推荐处理
null 能力不可用、尚未完成首次查询,或查询失败。 使用普通回退布局;不要据此断定当前没有遮挡。
空列表 查询成功,当前没有激活的 occlusion 区域。 保持普通布局。
非空列表 查询成功,包含当前激活的遮挡矩形。 对重要内容做相交判断,再决定如何避让。

返回的 Rect 使用 Flutter 窗口逻辑坐标。若目标组件位于局部坐标系,需要在布局完成后先把它的边界转换到窗口坐标,再调用 Rect.overlaps。下面的 targetContext 指向要保护的组件:

dart 复制代码
final renderObject = targetContext.findRenderObject();
if (renderObject is RenderBox && renderObject.hasSize) {
  final Rect targetInWindow = MatrixUtils.transformRect(
    renderObject.getTransformTo(null),
    Offset.zero & renderObject.size,
  );
  final bool isCovered = regions?.any(
        (Rect region) => region.overlaps(targetInWindow),
      ) ??
      false;
  // isCovered 时,按界面需要移动或重排目标组件。
}

Rect 描述的是局部遮挡,不等同于 EdgeInsets。给整条边统一增加 padding 可能浪费空间,也未必能准确避开遮挡。

整体架构

flowchart LR A[业务 Widget] -->|读取状态| B[ReservedRegionProvider] C[ReservedRegionBridge] -->|刷新并更新| B C -->|load| D[ReservedRegionChannel] D -->|MethodChannel| E[iOS Plugin] E -->|查询| F[Flutter UIView] F -->|UIViewReservedRegion| E E -->|矩形字典数组| D D -->|校验并转换为 Rect| C

主要组件各负责一段数据流:

组件 文件 职责
TwIphoneDuoReservedRegionBridge Bridge 源码 监听生命周期和窗口变化,触发查询并保存最新状态。
TwIphoneDuoReservedRegionProvider Provider 源码 通过 InheritedWidget 向子树提供结果,只在列表内容变化时通知。
TwIphoneDuoReservedRegionChannel Channel 源码 调用 MethodChannel、校验原生数据并转换为不可修改的 Rect 列表。
TwIphoneDuoReservedRegionPlugin iOS 插件源码 在主线程查询当前 Flutter View,并序列化激活的遮挡矩形。

查询数据流

Bridge 调用 Channel,Channel 通过 MethodChannel 请求原生数据。iOS 实现使用 [UIViewReservedRegionKind occlusionRegionKind] 查询 Flutter View,并以 region.isActive 过滤未激活区域。每个区域序列化为 left、top、width、height;Dart 层校验数组、数字和非负尺寸后转换为 Rect。非 iOS 平台直接返回 null。

MethodChannel 协议:

text 复制代码
channel: tw591/iphone_duo_reserved_regions
method:  getActiveOcclusionRegions
result:  array of {left, top, width, height} | null

生命周期刷新策略

Reserved Region 可能随窗口尺寸、设备状态或应用生命周期变化。Bridge 在以下时机刷新:

  • 首帧完成后:确保 Flutter View 已经建立并拥有有效几何信息。
  • didChangeMetrics:响应窗口尺寸和 metrics 变化。
  • AppLifecycleState.resumed:应用回到前台后重新同步系统状态。

若多次刷新并发,Bridge 只接受最新请求的结果,避免旧窗口几何信息覆盖新状态。

错误处理与安全回退

Channel 遇到 MissingPluginException、PlatformException、无效数组或矩形数据,以及其他未预期异常时,会调用 FlutterError.reportError 并返回 null。上层因此可以保持回退布局。

原生层在 Flutter View 不可用时返回带有 flutter_view_unavailable 错误码的 FlutterError;在 iOS 27.1 之前则返回 nil,对应 Dart 层的 null。

Swift Package Manager 与 CocoaPods

两种包管理器指向同一套 Objective-C 源码:

text 复制代码
ios/
├── tw_iphone_duo_reserved_region.podspec
└── tw_iphone_duo_reserved_region/
    ├── Package.swift
    └── Sources/
        └── tw_iphone_duo_reserved_region/
            ├── TwIphoneDuoReservedRegionPlugin.m
            └── include/
                └── tw_iphone_duo_reserved_region/
                    └── TwIphoneDuoReservedRegionPlugin.h

Package.swift 定义 Swift Package target,并依赖 Flutter 生成的 FlutterFramework。 podspec 则将 source_files 和 public_header_files 指向相同的 Sources 目录。

修复原生逻辑时只需修改这一份源码,两种集成方式会使用相同实现。

验证策略

插件的验证重点分为四层:

  1. Dart 单元测试:验证数据解析、无效数据回退和错误报告。
  2. Widget 测试:验证 Bridge 的首帧刷新、生命周期刷新、metrics 刷新和过期请求丢弃。
  3. 包管理器构建:分别通过 Swift Package Manager 与 CocoaPods 编译同一原生实现。
  4. 发布校验:使用 dart pub publish --dry-run 检查归档内容和包元数据。

常用检查命令:

console 复制代码
flutter analyze
flutter test
dart pub publish --dry-run

延伸阅读

相关推荐
百万蹄蹄向前冲1 小时前
一张平面图把学校做成2D游戏
前端·人工智能·后端
机器之心1 小时前
突发:Claude自主发现未知生物系统,或能编辑基因
前端·人工智能·后端
gnip1 小时前
UniApp 内嵌 H5 通信全攻略
前端·javascript
仿生狮子1 小时前
实现近乎免费之后,设计工程师还剩什么
前端·后端·设计
EatFans1 小时前
Electron 打包与自动更新完全指南:electron-builder、latest.yml、app-update.yml 与自建更新服务器实战
前端
小凯在掘金1 小时前
为什么要有访问器? 你不知道的对象属性
前端·javascript
OpenTiny社区1 小时前
HC 2026 回顾|OpenTiny NEXT 解锁 Web 应用智能化新范式
前端·开源·github
时光少年1 小时前
Android HWC退化与防治方法
前端
涛涛ing1 小时前
Remix 3 RC 发布:一个不再依赖 React 的全栈框架,正在重新定义“元框架”的边界
前端