跳转到内容

适配计划

基于前任 AI 的 HANDOVER.md 和最新的 bugfix-round-2-safearea/TASK.md,对状态栏遮挡按钮的问题进行深度分析,并提供两套可选的解决方案及其实施路径。


目前界面顶部按钮被状态栏(电量、时间等)挡住,问题既不在于按钮位置写错,也不在于状态栏配置错误,而是:

  1. 启用了沉浸式全屏:Android 端配置了状态栏透明,WebView 区域拉伸到了屏幕物理最顶端(y = 0)。
  2. 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,导致顶栏内容直接被盖在系统状态栏图标下方。

要解决这个问题,有以下两种完全不同的技术路径。请根据您的需求进行选择:

【方案 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 插件注入 - 视觉无边界)”

在前端根目录 andraw 执行:

Terminal window
pnpm add capacitor-plugin-safe-area
npx cap sync android
  • 新建文件路径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);
    };
    }, []);
    }
  • 文件路径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 变量的格式。

  1. 文件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-topbarheight 换成 height: calc(56px + var(--sat-top, env(safe-area-inset-top, 0px)))
    • .app-actionbarpadding-bottom 同理。
  2. 文件PhoneTopBar.tsx
    • 第 43 行:paddingTop 替换为 "var(--sat-top, env(safe-area-inset-top, 0px))"
    • 第 44 行:minHeight 里的 env 替换为同一变量。
  3. 文件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))
  4. 文件SolveActionBar.tsx
    • 第 32 行 paddingBottom 替换为 "calc(12px + var(--sat-bottom, env(safe-area-inset-bottom, 0px)))"

四、 编译与验证命令(适用于两套方案)

Section titled “四、 编译与验证命令(适用于两套方案)”

修改完毕后,在 andraw 根目录下执行以下步骤编译并检查:

Terminal window
# 1. 编译前端静态文件并同步至 Android 工程
pnpm build
npx cap sync android
# 2. 编译 APK
cd android
# 如果测试平板端:
./gradlew assembleTabletDebug
# 如果测试手机端:
./gradlew assemblePhoneDebug

真机验证检查单

  • 顶部按钮(新建拆解、返回、设置等)是否与状态栏图标完全分离开,间距正常(约有 24~28px 的状态栏安全高度)。
  • 底部操作条(运行代码等)在手势导航下是否没有被底部白条遮挡。
  • 浏览器端开发模式下(pnpm dev),页面布局是否依然完好,没有因为修改发生坍塌(方案 B 会自动在非原生平台回退到 env(),应保持完全一致)。