跳转至

移动端布局

移动端应用运行于手机、折叠屏、平板电脑以及可调整大小的 iPad 窗口上。有一条规则让这一点易于管理:下面的每一层各自做一个决策,且不做别的层的决策。功能界面永远不会询问自己处于哪个平台或窗口宽度。

宽窗口呈现为五个堆叠层:路由器、工作区外壳、功能界面、设计系统、原生层

紧凑型 Android 屏幕呈现相同的五个层

层级结构

  1. 路由层。 根堆栈 就是布局状态:当前选中了什么、推入了什么、哪个 sheet 处于打开状态。旋转、折叠或调整大小会以不同方式渲染同一个堆栈。没有任何地方会记住某个页面曾在哪一列中。每条路由通过一个 preset 声明自己的类型(pageRoute、sheetRoute、modalRoute、immersiveRoute);preset 负责 detents、圆角、头部外观以及 Android 回退行为。参见 移动端导航。
  2. 工作区外壳。 features/layout 负责放置位置和宽度,基于 layout.ts 中的宽度类别:

    • Expanded:侧边栏、内容和检查器三列。
    • Medium(宽 600–720pt):侧边栏位于列表旁,并浮动在页面之上。
    • Compact(任一边小于 600pt):侧边栏作为根,页面向后推入。
    • 当检查器列放不下时,检查器变成 sheet。

    工作区标签页、停靠栏和工具栏位于 features/layout/workspace。嵌套在 sheet 或 popover 中的堆栈,以及在自己的路由下渲染的界面,也都来自这里。

  3. 功能界面。 界面按角色放置内容。它会绘制一个 AppBar(如果是表单 sheet 则用 SheetHeader),渲染其页面,用 <Inspector> 声明检查器面板,并通过 showInspector() 请求显示它们。外壳决定使用列还是 sheet。如果在这里按宽度或平台来选择路由、组件、头部选项或 inset,正是此结构要防止的 bug。

  4. 设计系统。 每个面板、页面、sheet 和模态框都有一个 AppBar,在两个平台上遵循相同契约;sheet 或模态框传入 onClose。PaneContext 告诉栏它位于哪一列中,从而添加该列的控制项,例如侧边栏开关。内容与栏相接的位置由 useBarInset 决定。界面外观(Chrome)只在这里按平台分支。
  5. 原生层。 在 iOS 26 上,各列是 UISplitViewController 的列,每列各带一个导航栏。在 Android 以及 iPadOS 18–25 上,外壳根据窗口和铰链(hinge)指标,用 JS 绘制相同的列。Android 从不显示原生堆栈头部;每个栏都在组件树内绘制。

平台分支位于第 1、2、4 层,以 .ios 和 .android 文件实现。

陷阱

  • iPadOS 18–25 运行 JS 列,且 iOS 部署目标为 18。在 JS 列路径中看起来只属于 Android 的代码,在这些 iPad 上仍然生效。
  • Android 会渲染 Home 两次:一次在侧边栏列中,一次作为内容堆栈的根。侧边栏中的那份副本永远不会是处于聚焦状态的路由,因此以 focus 为门槛的 hooks(例如 Back 处理)还必须接受 usePaneRole() === "sidebar"。
  • 模态框和全屏页面呈现在各列之上,并且渲染时没有任何 pane role,因此不会获得列级界面装饰(chrome)。
  • 在 iOS 上,每个检查器面板都是独立的原生屏幕,并且只有当前活动的一个会被挂载。通过透明度隐藏的面板在窗口中仍保留其原生栏、工具栏和滚动视图,UIKit 会将它们显示在可见面板之上。
  • 从检查器 sheet 打开的路由必须通过 InspectorSheetContext 等待 sheet 关闭。在 iOS 上,如果 sheet 正在关闭时再在其上方呈现内容,会与它的关闭过程产生竞争(race)。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code