第 10 章:跨平台滚动、布局与 Slot 一致性
1. 共享的不是 View 类,而是不变量
Android 和 iOS 没有强行使用相同的原生 widget;它们共享以下 behavioral contract:
| 行为 | Android | iOS |
|---|---|---|
| snapshot decode | JSONObject + parser executor | JSONDecoder + parsing queue |
| owner change | reset pager/page viewport/refresh | reset pager/page viewport/refresh |
| tab | ViewPager2 | paging UIScrollView |
| body list | RecyclerView | UITableView |
| handoff | 无 page,发 intent | 无 page,发 intent |
| slot host | ViewGroup reparent | UIView reparent |
| owner mismatch | ownerAuthorized=false,吞触摸 | hidden/accessibility hidden,Surface hitTest 拒绝 |
| refresh | request id + completeRefresh | request id + completeRefresh |
2. Header collapse offset 是共同语言
用户看到的是一个页面,但实现中 header、tabs 和 body 可能由不同滚动容器承载。两端都需要一个抽象量:collapseOffset。
如果切 tab 时不把 offset 同步给目标页,用户会看到:当前页已经折叠,下一页却从 expanded header 开始;slot host 也会瞬间换高度。这是状态连续性 bug,而不只是动画问题。
3. Slot layout 的共同流程
这个流程必须可重复、幂等:同一个 slot 可能因为列表复用、tab attach、dynamic type 或 owner 变化被多次 layout。代码使用“当前 parent 是否为 host”“当前尺寸是否一致”“mounted keys 是否变化”等检查减少无意义 reparent。
4. React slot 的交互模式
wrapper 为 slot 设置两层 pointerEvents:
tsx
const interactive = slot.interaction === 'tap';
<HomeContainerSlot pointerEvents={interactive ? 'auto' : 'none'}>
<View pointerEvents={interactive ? 'auto' : 'none'}>{slot.content}</View>
</HomeContainerSlot>这表达了一个重要分工:
interaction: none的 slot 仍可作为视觉内容挂载,但不能截获 native scroll/tap;interaction: tap允许 React 内容响应,比如账号选择、余额刷新、空状态按钮;- native engine 自己的 action row、tabs、list item 仍然通过 native intent 产生事件。
5. 背景色优先级
resolveHomeContainerBackgroundColor 规定:snapshot theme 的 background 优先于 slot 提供的 fallback,只有两者都没有时才用白色。这样在 state 尚未到达、React slot 已到达时仍有视觉背景;一旦 authoritative snapshot 到达,slot 不会覆盖主题。
6. 跨平台调试方法
推荐按三层打点,而不是只截图:
- JS:记录 snapshot 的 owner、selectedTab、tab destination、slot keys;
- bridge:记录 state decode error、intent JSON、requestId;
- native:记录 page attach/detach、collapse offset、slot host size、owner authorization。
工程里已有 debugOverlayEnabled,Android 用洋红色 border/background,iOS 用 border;它是检查 Surface bounds 和 engine 实际占位的低成本工具。