适配计划
基于前任 AI 的 HANDOVER.md 和最新的 bugfix-round-2-safearea/TASK.md,对状态栏遮挡按钮的问题进行深度分析,并提供两套可选的解决方案及其实施路径。
一、 问题根源深度分析
Section titled “一、 问题根源深度分析”目前界面顶部按钮被状态栏(电量、时间等)挡住,问题既不在于按钮位置写错,也不在于状态栏配置错误,而是:
- 启用了沉浸式全屏:Android 端配置了状态栏透明,WebView 区域拉伸到了屏幕物理最顶端(
y = 0)。 - Android WebView 的 CSS 安全区域(Safe Area)缺陷:
- iOS 平台下,WKWebView 会自动给
env(safe-area-inset-top)注入真实的状态栏高度。 - Android 平台的 WebView 则不然:它对
env(safe-area-inset-top)的支持极不稳定。在许多平板或无刘海屏手机上,只要没有物理“刘海/挖孔”,这个值就会被解析为0px。 - 因此,项目中所有依赖
env(safe-area-inset-top, 0px)的paddingTop实际都变成了0px,导致顶栏内容直接被盖在系统状态栏图标下方。
- iOS 平台下,WKWebView 会自动给
二、 核心抉择:方案对比分析
Section titled “二、 核心抉择:方案对比分析”要解决这个问题,有以下两种完全不同的技术路径。请根据您的需求进行选择:
【方案 A】关闭 WebView 穿透(系统原生级修复,推荐 🌟🌟🌟)
Section titled “【方案 A】关闭 WebView 穿透(系统原生级修复,推荐 🌟🌟🌟)”让 WebView 退回到系统状态栏下方(不穿透),状态栏区域由 Android 系统自动占位。
- 技术原理:关闭 WebView 延伸到状态栏底下的配置。状态栏会变成一条实心的颜色块,WebView 从状态栏下方开始渲染。状态栏背景色可以通过前端设置为匹配应用背景的暗灰色或浅灰色。
- 开发难度:极低。只需修改 2 处(1 行 Java,1 行 TS)。
- 成功率:100%。这是 Android OS 系统的原生行为,绝对稳定,不受机型、刘海屏、软键盘或旋转影响。
- 视觉效果:非无边界全屏,顶部会有一条系统状态栏占位色块。在平板横屏模式下,这其实是标准且更符合直觉的体验。
【【方案 B】插件实测注入 Safe-Area(前端布局级修复,见 TASK.md)
Section titled “【【方案 B】插件实测注入 Safe-Area(前端布局级修复,见 TASK.md)”保留 WebView 穿透(沉浸式全屏),使用 Capacitor 原生插件获取真实状态栏像素高度,然后通过 JS 注入为 CSS 变量,替换所有的 env()。
- 技术原理:安装
capacitor-plugin-safe-area插件,在 App 初始化时调用原生接口读取 top/bottom 真实的安全区像素高度,通过--sat-top等 CSS 变量全局注入到document.documentElement,前端 CSS 优先读取该变量。 - 开发难度:较高。需要引入新第三方插件、同步 Android 工程、编写 React Hook 监听屏幕旋转与尺寸变化,并修改 10+ 个前端组件文件的 CSS 样式。
- 成功率:~90%。在某些高度定制的国产平板系统(如小米、华为等)上,由于系统层对 Inset 汇报口径不一,偶有获取高度为 0 或失效的风险。
- 视觉效果:最完美。保留了应用背景色直接穿透延伸到状态栏底下的现代“无边界全屏”效果。
三、 执行计划(二选一,直接供其他 AI 读取执行)
Section titled “三、 执行计划(二选一,直接供其他 AI 读取执行)”请选择您偏好的方案,并将对应的实施计划提供给其他 AI 执行:
🛠️ 方案 A 实施计划(系统原生级修复 - 极简高效)
Section titled “🛠️ 方案 A 实施计划(系统原生级修复 - 极简高效)”步骤 1:修改前端原生配置 useNativeChrome.ts
Section titled “步骤 1:修改前端原生配置 useNativeChrome.ts”-
文件路径:useNativeChrome.ts
-
修改动作:将
overlay: true改为overlay: false,使得 WebView 不穿透状态栏。 -
代码对照:
await StatusBar.setOverlaysWebView({ overlay: true });await StatusBar.setOverlaysWebView({ overlay: false });
步骤 2:修改 Android 原生入口 MainActivity.java
Section titled “步骤 2:修改 Android 原生入口 MainActivity.java”-
文件路径:MainActivity.java
-
修改动作:将系统装饰自适应设为
true,防止 WebView 在底层自动延伸。 -
代码对照:
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
🛠️ 方案 B 实施计划(Safe-Area 插件注入 - 视觉无边界)
Section titled “🛠️ 方案 B 实施计划(Safe-Area 插件注入 - 视觉无边界)”步骤 1:安装依赖并同步
Section titled “步骤 1:安装依赖并同步”在前端根目录 andraw 执行:
pnpm add capacitor-plugin-safe-areanpx cap sync android步骤 2:创建 Safe-Area 注入 Hook
Section titled “步骤 2:创建 Safe-Area 注入 Hook”-
新建文件路径:
andraw/src/app/useSafeAreaInsets.ts -
代码内容:
import { useEffect } from "react";import { Capacitor } from "@capacitor/core";import { SafeArea } from "capacitor-plugin-safe-area";export function useSafeAreaInsets() {useEffect(() => {if (!Capacitor.isNativePlatform()) return;let cancelled = false;const apply = async () => {try {const { insets } = await SafeArea.getSafeAreaInsets();if (cancelled) return;const d = document.documentElement;d.style.setProperty("--sat-top", `${insets.top}px`);d.style.setProperty("--sat-right", `${insets.right}px`);d.style.setProperty("--sat-bottom", `${insets.bottom}px`);d.style.setProperty("--sat-left", `${insets.left}px`);} catch (e) {console.warn("[useSafeAreaInsets] failed, fallback to env()", e);}};apply();const onResize = () => apply();window.addEventListener("resize", onResize);window.addEventListener("orientationchange", onResize);return () => {cancelled = true;window.removeEventListener("resize", onResize);window.removeEventListener("orientationchange", onResize);};}, []);}
步骤 3:在入口挂载 Hook
Section titled “步骤 3:在入口挂载 Hook”-
文件路径:App.tsx
-
修改动作:导入并执行
useSafeAreaInsets(),使 CSS 变量全局生效。 -
代码对照:
import { useSafeAreaInsets } from "./app/useSafeAreaInsets";function Inner() {useSafeAreaInsets();return <AppShell />;}export default function App() {return (<AppProviders><div style={{ height: "100%", minHeight: 0, display: "flex", flexDirection: "column" }}><AppShell /></div><div style={{ height: "100%", minHeight: 0, display: "flex", flexDirection: "column" }}><Inner /></div></AppProviders>);}
步骤 4:替换全局及组件内的 CSS Inset 属性
Section titled “步骤 4:替换全局及组件内的 CSS Inset 属性”将原先所有的 env(safe-area-inset-*) 替换为优先读取 CSS 变量的格式。
- 文件:base.css
.pt-safe替换为padding-top: var(--sat-top, env(safe-area-inset-top, 0px));.pb-safe替换为padding-bottom: var(--sat-bottom, env(safe-area-inset-bottom, 0px));.pl-safe替换为padding-left: var(--sat-left, env(safe-area-inset-left, 0px));.pr-safe替换为padding-right: var(--sat-right, env(safe-area-inset-right, 0px));.app-topbar里的padding-top换成相同变量格式。.app-topbar的height换成height: calc(56px + var(--sat-top, env(safe-area-inset-top, 0px)))。.app-actionbar的padding-bottom同理。
- 文件:PhoneTopBar.tsx
- 第 43 行:
paddingTop替换为"var(--sat-top, env(safe-area-inset-top, 0px))" - 第 44 行:
minHeight里的env替换为同一变量。
- 第 43 行:
- 文件:TabletShell.tsx
- 第 100 行
paddingTop改为var(--sat-top, env(safe-area-inset-top, 0px)) - 第 119 行
paddingBottom改为var(--sat-bottom, env(safe-area-inset-bottom, 0px)) - 第 123 行
paddingLeft改为var(--sat-left, env(safe-area-inset-left, 0px))
- 第 100 行
- 文件:SolveActionBar.tsx
- 第 32 行
paddingBottom替换为"calc(12px + var(--sat-bottom, env(safe-area-inset-bottom, 0px)))"
- 第 32 行
四、 编译与验证命令(适用于两套方案)
Section titled “四、 编译与验证命令(适用于两套方案)”修改完毕后,在 andraw 根目录下执行以下步骤编译并检查:
# 1. 编译前端静态文件并同步至 Android 工程pnpm buildnpx cap sync android
# 2. 编译 APKcd android# 如果测试平板端:./gradlew assembleTabletDebug# 如果测试手机端:./gradlew assemblePhoneDebug真机验证检查单:
- 顶部按钮(新建拆解、返回、设置等)是否与状态栏图标完全分离开,间距正常(约有 24~28px 的状态栏安全高度)。
- 底部操作条(运行代码等)在手势导航下是否没有被底部白条遮挡。
- 浏览器端开发模式下(
pnpm dev),页面布局是否依然完好,没有因为修改发生坍塌(方案 B 会自动在非原生平台回退到 env(),应保持完全一致)。