The drawer bar was described across two iOS bullets, a page bullet and an upstream-sync entry. MobileStyleAdaptation now gives it one section: placement, the snapshot as its single source, publishing, activation and page styling. API/Layout keeps what extension and theme authors observe, iOSDevelopment keeps the simulator notes, and tt-docs is regenerated. Validation: pnpm run check.
12 KiB
移动端内容视口与窗口背景
移动端的系统栏、刘海和停靠键盘由原生宿主在内容视口层消费一次。网页内没有系统栏或键盘适配机制:第一方与扩展都按普通浏览器规则排版。
两份几何事实
| 事实 | 内容 | 变化时机 | 消费者 |
|---|---|---|---|
| 窗口快照 | 窗口物理像素尺寸、政策避让、scale、revision、抽屉栏所在边 | 旋转、窗口尺寸、显示政策变化 | #bg1 窗口矩形、原生背景条带、第一方竖屏判断、抽屉栏模式 |
| 内容视口 | 窗口减去政策避让与停靠键盘 | 上述变化,以及键盘出现、消失、高度变化 | 浏览器布局引擎 |
窗口快照不含键盘。键盘只改变内容视口的底边,顶部固定;键盘过渡不产生 JS 背景发布、:root 变量写入或图片工作。浮动与分离键盘不报告 inset,不缩小视口。
一次键盘过渡只让内容视口变化一次:宿主取键盘的目标几何,在过渡开始时提交(iOS 的交互拖动在松手时提交),键盘在已就位的页面上方滑入或滑出,露出的区域显示宿主底色。逐帧跟随会让浏览器引擎逐帧重排。
Android
入口 AndroidWindowLayout.kt,由 MainActivity 编排生命周期,WindowLayoutPlugin 供 Rust 调用。Rust 的 platform/window_layout_plugin.rs 与鸿蒙共用请求、应答和条带传输,只在插件注册时选择平台入口。
- edge-to-edge 只由
WindowCompat.enableEdgeToEdge(window)配置。 - 政策避让:非沉浸为稳定的 systemBars ∪ displayCutout,沉浸为零。用户偏好
power_user.mobile_immersive_fullscreen经mobile-system-ui.js与AndroidSystemUiJsBridge传给原生。新安装及缺少该字段的设置默认非沉浸,已保存的选择保持不变。 - 主 WebView 矩形由一个函数计算:四边取政策避让,底边取政策避让与 IME 底部 inset 的较大值;相同矩形不重复提交。
- 提交时机:只跟随
onApplyWindowInsets。键盘动画开始时系统已按终态下发 insets;宿主不注册 insets 动画回调,否则平台改在主线程逐帧驱动键盘动画,Android 16 的预测性返回也会在拖动中逐帧改布局。预测性返回收键盘在松手确认时提交一次,取消时不变。 - 内容根节点把原始 insets 传给子视图。主 WebView 自己的监听器把系统栏、刘海与 IME 清零:API 31+ 交给 WebView 的
onApplyWindowInsets;API 28–30 上它替换了 Chromium 构造时安装的监听器,引擎不接收 inset。 - 元素全屏是独立的展示状态:隐藏系统栏,由兄弟容器覆盖主页面,不改变用户偏好、窗口快照或 WebView 矩形。临时滑出的系统栏同样不改变政策。
- 旋转等由
configChanges原位处理。Activity 重建(如切换开发者刘海选项)后 Tauri 插件仍持有旧 Activity,窗口插件拒绝请求,需重启应用。 - 窗口背景画在 decorView 的背景上:底色加条带。状态栏与导航栏图标经
WindowInsetsControllerCompat设置;导航栏按 navigationBars inset 所在的边取底边或侧边条带。
iOS
入口 infrastructure/ios_webview.rs,安装 platform/ios_window_layout.rs 的窗口宿主 UIView。
- WKWebView 上、左、右约束到宿主的内容 guide,底边约束到
keyboardLayoutGuide.topAnchor。键盘收起时 keyboard guide 停在底部安全区。 - iOS 没有沉浸模式,政策避让就是内容 guide:平时等于安全区;有抽屉栏时页面没有顶栏,上边改用纵向角落适配的安全区(
safeAreaLayoutRegionWithCornerAdaptation:),避开显示圆角。快照的 insets 读同一个 guide。 - 内部 UIScrollView 保持
contentInsetAdjustmentBehavior = .never与零 inset;WebKit 自己的焦点、选区与键盘处理照常运行。 - 主窗口隐藏 WebKit 表单附属栏(
disableInputAccessoryView)。收键盘手势由宿主内的全窗口KeyboardScrollView提供:UIKit.interactive,与网页内部滚动视图的 pan 同时识别,不取消内容触摸,网页控件操作与键盘跟手可以同时发生。容器没有滚动内容,setContentOffset:固定为零,alwaysBounceVertical只用来让 pan 能够开始。 - WKWebView 是容器的子视图,约束对象仍是宿主的 guide。UIKit 在键盘动画块里一次设定 guide;交互拖动不同,guide 跟手变化,所以容器的 pan 活动期间 WebView 高度保持不变,松手后恢复跟随。iOS WebKit 把每次 WebView 尺寸变化当作样式表环境变化整文档重建样式,逐帧跟随的代价尤其高。
- 宿主在
layoutSubviews按值去重发布窗口快照。底色设在宿主视图上,条带是插在 WKWebView 之下的 UIImageView。 - 状态栏样式经根控制器的
preferredStatusBarStyle生效。tao 没有提供这个入口,宿主安装时把它加到TaoUIViewController上;tao 自带同名方法时安装报错,改用 tao 的实现。有系统竖栏时状态栏在竖栏里,图标颜色取该侧条带。home indicator 由系统自适应。 - 元素全屏时 WebKit 会把 WKWebView 移出容器;归还时容器停用保持约束并重新激活同一组边约束。全屏期间手势不由容器提供;普通 CSS 全屏面板、dialog 和嵌入的 iframe 仍在覆盖范围内。
鸿蒙
入口 gen/ohos/entry/src/main/ets/WindowLayout.ets,由 EntryAbility 持有,作为应用自有的 window-layout 插件接入原生插件桥。
- ArkUI 负责安全区布局:
setWindowLayoutFullScreen(false)加module.json5的avoid_cutout,覆盖系统栏、导航指示条和刘海。鸿蒙没有沉浸模式,政策避让始终是安全区。 - Ability HAR 的 Web 组件使用
RESIZE_CONTENT:ArkWeb 按键盘与 Web 的重叠缩小布局视口,Web 外框不变。窗口管理器每次停靠键盘过渡只下发一次终态占用区,ArkWeb 再把 20 ms 内的重复上报合并为一次,所以布局视口同样一次到位,宿主不另做键盘处理。 - 快照由窗口尺寸、上述三类避让区和 UIContext 像素比例组成,不含键盘,按值去重。页面请求过初始快照后,变化经
WebHost.runJavaScript发布。 - 背景层是
pages/Main中 Web 的兄弟节点,以LayoutPolicy.matchParent加ignoreLayoutSafeArea()铺满窗口。PNG 解码是异步的,解码完成后才核对 revision 与令牌;没有采用的 PixelMap 都释放。 - 状态栏与三键导航栏的内容色由一次
setWindowSystemBarProperties局部更新设置。手势导航指示条没有公开的设色接口,由系统控制。 - 网页全屏沿用 HAR 的临时全屏布局,系统栏仍然可见。
窗口背景
src/scripts/window-backdrop.js 的 applyWindowBackdrop() 是第一方背景的唯一写入点:全局背景、聊天锁定背景、聊天自定义与生成图片、填充方式、主题底色、自定义 CSS 与 OLED 开关都经过它。新增第一方背景来源也接入它。
src/scripts/util/window-layout.js 把窗口快照写成 #bg1 上的 --tt-window-x/-y/-width/-height。#bg1 固定为窗口矩形并使用 background-attachment: scroll,键盘不改变壁纸坐标。移动端壁纸切换不做渐变,两个渲染者始终呈现同一外观。
set_window_backdrop 的顺序:
- JS 按帧合并,壁纸输入与底色分别去重;只改底色时不发送壁纸。
#bg1不可见(如 OLED)时清除壁纸。 - 原生核对窗口 revision 后立即更新底色。壁纸变化时生成新令牌并清除旧条带,返回自己的窗口快照与令牌。这一步不等待解码,新选择立即使旧渲染失效。
tt-adapter-media::window_backdrop一次只解码一张图:经 Host Resource Store 读取,取首帧并应用 EXIF 方向,按background-size/background-position映射到窗口,按预乘 alpha 采样,输出透明条带。每条条带带有所在的边和平均色(预乘 RGB 与 alpha,均为 0..1,透明像素计入)。- 原生在窗口 revision 与令牌都匹配时整体替换条带,过期结果直接丢弃。
系统栏图标三端同一规则:取该边条带的平均色,加上 (1 − 平均 alpha) × 底色,没有条带时取底色;按 sRGB 线性化与 Rec. 709 权重计算相对亮度,大于 0.179 时用深色图标。底色变化、条带替换或清空时重新计算,结果变化才设置。平均色属于条带记录,随条带一起接受与清除,所以只换底色不需要重新解码。
这是近似:换壁纸时图标可能先按底色、再按新条带切换一次;系统栏覆盖网页或视频时(Android 沉浸模式滑出、元素全屏),仍按宿主背景判断。
系统栏条带不呈现:视频壁纸(含第一方 mp4)、渐变与外部 URL(只画底色并记录诊断)、弹窗遮罩、主题滤镜与第三方背景层。动画图片取首帧。条带不做磁盘缓存。
抽屉栏(iOS 27.1+)
系统在内容视口之外给出 bar 区域时,宿主把页面顶层抽屉的入口画成原生玻璃胶囊(UIGlassEffect 与 SF Symbol 按钮),页面不画顶栏。宿主在 platform/ios_drawer_bar.rs,页面在 src/scripts/drawer-bar.js。
- 位置:
verticalBarEdge报告有竖栏时用竖栏,否则用状态栏旁的顶部带,都没有时保留页面顶栏。具体的边取layoutRegionForBarOnEdge:extent:区域整块落在哪条边的安全区 inset 内;trait 只给 leading/trailing,旋转时还晚一次布局,不用它定边。胶囊从区域起点排起,放不下时按钮等分缩小。 - 事实来源:快照的
drawerBar.edge(left/right/top)。页面模式(根元素的data-tt-drawer-bar)、内容 guide 的上边与状态栏取色的条带都由它决定。 - 发布:页面经
set_drawer_bar_items发布每个可见的顶层抽屉(id、标题、开合、标记),抽屉增删、开合、标题或标记变化时按帧合并。标记为alert(API 断开)、working与unread(助手)。SF Symbol 按抽屉 id 对应,扩展抽屉用通用符号;胶囊明暗跟随状态栏样式。 - 激活:原生点按派发
tt-drawer-bar-activate,页面点击该抽屉的图标,开合与 hydration 走原有路径;页面有模态弹窗时忽略。 - 页面样式:
src/css/tauritavern-drawer-bar.css隐藏顶栏与抽屉开关,--topBarBlockSize为 0,抽屉从栏所在的边平移滑入。
第一方行为
--doc-height是静态别名:支持时100dvh,否则100vh。- 只有高度变化的 resize(键盘)不触发与宽度相关的工作:上游 resize 处理器在移动宿主上早退,聊天虚拟化度量按宽度门控,Quick Reply 编辑器只在宽度变化时 blur/focus。movingUI 缩放按宿主身份
isMobileHost()判断。 - 聊天宽度:移动宿主且窗口竖屏时固定为 100%,滑块禁用;竖屏读窗口快照,不读视口。
- 输入框聚焦(
src/scripts/chat-input-focus.js):移动宿主拒绝导航与恢复类聚焦,只在编辑意图下弹出键盘;Android 进入后台时让输入框失焦。 - 第一方弹窗用自身类名的普通 CSS 管理尺寸;聊天列表自己负责滚动。
- 主题 CSS 按普通层叠生效,宿主不改写主题对第一方容器的几何。
与上游的本地差异
同步上游时保留这些删除:
src/index.html中写入--doc-height像素值的 resize 脚本。src/style.css中html上的 transform 与 perspective:它们让html成为 fixed 元素的包含块,根文档滚动时#bg1会随之移动。src/css/mobile-styles.css中#bg1的100dvw/100dvh !important尺寸:它会压过#bg1的窗口矩形。src/scripts/browser-fixes.js中 iOS resize 时把根元素临时设为position: fixed的补偿。- 顶层抽屉(带
openDrawer/closedDrawer)开合时的高度动画:src/style.css中.drawer-content的height: 0、打开时的calc-size(auto, size)与.fillLeft/.fillRight的高度过渡,以及src/css/mobile-styles.css中左右面板关闭时的height: 0。WebKit 不支持calc-size(),高度在过渡一半时跳变;抽屉改为按最终尺寸排版一次,隐藏态由--tt-drawer-*变量给出:默认自上而下clip-path揭开,抽屉栏模式改为平移。浮动面板仍沿用上游的高度过渡。随之删除打开时隐藏滚动条的hide-scroll动画:它结束时滚动条才出现,内容宽度会抽动。
公开布局 API 与旧变量的状态见 API/Layout.md。