Vue3 Webview 转 Android 虚拟导航栏遮挡问题记录

问题描述

在 Android 设备上运行 Capacitor 打包的 Vue 3 应用时,遇到虚拟导航栏(底部返回键、主页键等)和状态栏遮挡应用内容的问题。

问题表现

  • 底部 Tab 导航栏被虚拟导航栏遮挡一部分
  • 顶部内容被状态栏遮挡
  • 页面底部内容贴近虚拟导航栏,没有安全间距

问题根源分析

初始状态

应用使用了沉浸式布局,在 MainActivity.java 中设置了:

WindowCompat.setDecorFitsSystemWindows(getWindow(),false);getWindow().setStatusBarColor(Color.TRANSPARENT);getWindow().setNavigationBarColor(Color.TRANSPARENT);

这使得 WebView 内容延伸到状态栏和导航栏后面,实现了全屏显示。

错误的假设

最初尝试使用 CSS 的环境变量来解决:

padding-top:env(safe-area-inset-top, 0);padding-bottom:env(safe-area-inset-bottom, 0);

问题:Android WebView 不支持 CSS 的 env(safe-area-inset-*) 环境变量

实验验证

通过添加调试日志,在控制台输出:

// 检查 CSS 环境变量const rootStyles =getComputedStyle(document.documentElement) console.log('CSS 环境变量原始值:',{'safe-area-inset-top': rootStyles.getPropertyValue('safe-area-inset-top'),'safe-area-inset-bottom': rootStyles.getPropertyValue('safe-area-inset-bottom'),})// 检查实际效果 console.log('body paddingTop:',getComputedStyle(document.body).paddingTop) console.log('body paddingBottom:',getComputedStyle(document.body).paddingBottom)

实验结果

🔍 [CSS 环境变量] 原始值:[object Object] - safe-area-inset-top: "" (空字符串) - safe-area-inset-bottom: "" (空字符串) 🔍 [实验结论]: ❌ CSS 环境变量未生效 - 硬性编码起作用 

结论

  • CSS env() 环境变量在 Android WebView 中返回空值
  • 之前看到的 padding 效果是 CSS 中硬性编码的数值在起作用
  • 底部的 env(safe-area-inset-bottom, 0) fallback 到 0,导致没有底部间距

最终解决方案

方案核心:JavaScript 动态估算 + CSS 变量

由于 CSS 环境变量不生效,改用 JavaScript 在运行时动态计算安全区域高度,并通过 CSS 变量传递给样式层。

实现步骤

1. JavaScript 动态计算(src/main.ts)
// Capacitor 安全区域适配constsetupSafeArea=async()=>{try{const{ Capacitor }=awaitimport('@capacitor/core')// 只在原生平台上执行if(!Capacitor.isNativePlatform()){console.log('ℹ️ 非原生平台,跳过设置')return}console.log('🔍 [安全区域] 检测到原生平台,开始设置...')// 获取屏幕尺寸const screenWidth = window.screen.width const screenHeight = window.screen.height // 通过屏幕比例估算状态栏和导航栏高度// 状态栏通常是屏幕高度的 3-5% 或固定值(约 24-50dp)// 导航栏通常是 48-56dpconst estimatedStatusBarHeight = Math.min( Math.round(screenHeight *0.04),// 4% 屏幕高度50// 最大 50px)const estimatedNavBarHeight = Math.min( Math.round(screenHeight *0.05),// 5% 屏幕高度56// 最大 56px)console.log('🔍 [安全区域] 屏幕尺寸:', screenWidth,'x', screenHeight)console.log('🔍 [安全区域] 估算值 - 状态栏:', estimatedStatusBarHeight,'导航栏:', estimatedNavBarHeight)// 设置 CSS 变量const root = document.documentElement root.style.setProperty('--sat',`${estimatedStatusBarHeight}px`) root.style.setProperty('--sab',`${estimatedNavBarHeight}px`)// 更新 body 的 paddingconst body = document.body body.style.paddingTop =`${estimatedStatusBarHeight}px` body.style.paddingBottom =`${estimatedNavBarHeight}px`console.log('✅ [安全区域] 已设置 - 顶部:', estimatedStatusBarHeight,'底部:', estimatedNavBarHeight)}catch(e:any){console.log('⚠️ [安全区域] 设置失败:', e.message)}}// 应用挂载时执行 app.mount('#app').$nextTick(async()=>{awaitsetupSafeArea()// ... 其他初始化逻辑})
2. CSS 变量应用

将所有使用 env(safe-area-inset-*) 的地方替换为 var(--sat, 0)var(--sab, 0)

src/style.css

body{/* 顶部和底部安全区域通过 JS 动态设置 CSS 变量 */padding-top:var(--sat, 0);padding-bottom:var(--sab, 0);transition: padding-top 0.3s ease, padding-bottom 0.3s ease;}/* 页面容器 */.page-container{padding: 16px;padding-top:calc(16px + var(--sat, 0));padding-bottom:calc(80px + var(--sab, 0));min-height: 100vh;min-height: 100dvh;}

src/App.vue

.app-container{width: 100%;height: 100%;min-height: 100vh;min-height: 100dvh;position: relative;padding-top:var(--sat, 0);/* 顶部安全区域 */padding-bottom:calc(80px + var(--sab, 0));/* 底部安全区域 + 导航栏高度 */box-sizing: border-box;}.tab-bar{position: fixed;bottom: 0;left: 0;right: 0;height: auto;min-height:calc(56px + var(--sab, 0));display: flex;justify-content: space-around;align-items: flex-start;background: #fff;border-top: 1px solid #f0f0f0;z-index: 1000;padding-top: 10px;padding-bottom:calc(10px + var(--sab, 0));box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);}

src/views/Login.vue

.login-container{padding-top:calc(16px + var(--sat, 0));padding-bottom:calc(32px + var(--sab, 0));}.login-form{padding: 36px 32px;padding-top:calc(36px + var(--sat, 0));padding-bottom:calc(32px + var(--sab, 0));}

src/views/Classmates.vue 和 src/views/Tasks.vue

.page-container{padding: 16px;padding-top:calc(16px + var(--sat, 0));padding-bottom:calc(120px + var(--sab, 0));}

src/views/Tasks.vue (浮动按钮):

.fab-button{position: fixed;bottom:calc(90px + var(--sab, 0));right: 16px;}
3. Android 原生层配置(MainActivity.java)

保持沉浸式布局,但不隐藏系统栏:

@OverridepublicvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);// 启用边缘到边缘显示 (沉浸式)WindowCompat.setDecorFitsSystemWindows(getWindow(),false);// 设置透明状态栏和导航栏setupTransparentSystemBars();}privatevoidsetupTransparentSystemBars(){// 设置状态栏和导航栏透明getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT);getWindow().setNavigationBarColor(android.graphics.Color.TRANSPARENT);// 设置窗口标志以实现沉浸式int flags =WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;getWindow().setFlags(flags, flags);// 设置为边缘到边缘布局,但不隐藏系统栏// 让 CSS 的 safe-area-inset 来处理间距View decorView =getWindow().getDecorView();int uiOptions =View.SYSTEM_UI_FLAG_LAYOUT_STABLE |View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; decorView.setSystemUiVisibility(uiOptions);}@OverridepublicvoidonWindowFocusChanged(boolean hasFocus){super.onWindowFocusChanged(hasFocus);if(hasFocus){// 窗口获得焦点时,重新应用边缘到边缘布局View decorView =getWindow().getDecorView();int uiOptions =View.SYSTEM_UI_FLAG_LAYOUT_STABLE |View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; decorView.setSystemUiVisibility(uiOptions);}}

关键点

  • 移除了 SYSTEM_UI_FLAG_FULLSCREENSYSTEM_UI_FLAG_HIDE_NAVIGATION
  • 移除了 SYSTEM_UI_FLAG_IMMERSIVE_STICKY
  • 只保留 LAYOUT_FULLSCREENLAYOUT_HIDE_NAVIGATION 实现边缘到边缘布局
  • 不隐藏系统栏,让内容延伸到系统栏后面,通过 padding 留出安全区域
4. HTML viewport 配置(index.html)
<metaname="viewport"content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"/>

添加 viewport-fit=cover 启用全面屏适配。

1. 为什么 CSS env() 不生效?

  • CSS 的 env(safe-area-inset-*) 是 iOS Safari 引入的特性
  • Android WebView 和 Chrome 对此支持不完善
  • 在 Android 上需要手动实现安全区域适配

2. 为什么使用估算而非精确值?

  • Capacitor 的 StatusBar 和 NavigationBar 插件需要额外安装
  • 为了减少依赖,使用屏幕比例估算
  • 经验值:状态栏 ≈ 4% 屏幕高度,导航栏 ≈ 5% 屏幕高度
  • 设置上限避免极端情况(状态栏最大 50px,导航栏最大 56px)

3. CSS 变量的优势

  • 一次设置,全局使用
  • 支持动态更新
  • 保持样式层简洁
  • fallback 机制:var(--sat, 0) 确保非原生平台正常显示

4. 沉浸式布局的关键

  • WindowCompat.setDecorFitsSystemWindows(getWindow(), false) 启用边缘到边缘
  • 透明状态栏和导航栏
  • 使用 LAYOUT_FULLSCREENLAYOUT_HIDE_NAVIGATION 让内容延伸到系统栏后面
  • 不隐藏系统栏,而是通过 padding 留出安全区域

参考资料

Read more

卷积神经网络(CNN)进阶:经典架构解析与实战开发

卷积神经网络(CNN)进阶:经典架构解析与实战开发

卷积神经网络(CNN)进阶:经典架构解析与实战开发 💡 学习目标:掌握CNN的经典进阶架构设计思路,理解不同架构的核心创新点,能够基于经典架构开发定制化图像任务模型。 💡 学习重点:LeNet-5、AlexNet、VGGNet、ResNet的核心结构与改进逻辑,基于PyTorch实现ResNet-50并完成图像分类任务。 49.1 卷积神经网络进阶的核心驱动力 卷积神经网络从最初的简单结构发展到深度模型,核心驱动力是解决深层网络的性能瓶颈和提升特征提取的效率与精度。 在早期CNN的应用中,研究人员发现两个关键问题: 1. 网络深度增加到一定程度后,会出现梯度消失或梯度爆炸问题,导致模型无法收敛。 2. 简单堆叠卷积层的方式,会造成特征冗余和计算资源浪费,模型泛化能力受限。 ⚠️ 注意:CNN的进阶过程不是单纯的“堆层数”,而是通过结构创新、参数优化和训练技巧的结合,实现性能的突破。 ✅ 结论:经典CNN架构的每一次升级,都针对当时的技术痛点提出了创新性解决方案,掌握这些方案的设计思路,比记住网络结构更重要。 49.2 经典CNN架构深度解析 49.2.1

By Ne0inhk
2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线

2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线

2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线 前言 最近把自己的全栈博客项目部署到了腾讯云的入门级服务器(2核2G),过程中踩了不少坑。本文记录完整的部署过程和问题排查思路,希望对同样在小规格服务器上部署 Java 项目的同学有所帮助。 项目技术栈: * 后端:Java 17 + Spring Boot 3.2.3 + Spring Security + JPA * 数据库:MySQL 8.0 * 前端:Flutter Web * 反向代理:Nginx 1.26 * 容器:Docker 28.4 服务器配置: * 腾讯云轻量应用服务器 * 2 核 CPU / 2GB 内存 / 50GB

By Ne0inhk
SkyWalking - Spring Cloud Alibaba 全链路追踪实战

SkyWalking - Spring Cloud Alibaba 全链路追踪实战

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕SkyWalking这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * SkyWalking - Spring Cloud Alibaba 全链路追踪实战 🚀 * 1. 环境准备与核心概念 🧰 * 1.1 核心概念解析 * 1.2 环境准备 * 2. 构建 Spring Cloud Alibaba 微服务项目 🏗️ * 2.1 创建父工程 * 2.2 构建 `inventory-service`(库存服务) * 2.3 构建 `order-service`(订单服务) * 2.4 验证基础功能 * 3. 集成

By Ne0inhk
企业级web影院订票系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

企业级web影院订票系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着互联网技术的快速发展和数字化生活的普及,影院行业正面临着从传统线下售票向线上智能化转型的需求。观众对便捷、高效的购票体验要求日益提升,而影院管理方也需要一套功能完善、操作简便的系统来优化票务管理、排片安排和数据分析。基于此背景,开发一款企业级Web影院订票系统具有重要的现实意义。该系统能够整合线上线下资源,实现用户自助选座购票、影院动态排片、数据统计分析等功能,为影院运营提供全方位支持。关键词:影院订票系统、数字化转型、线上购票、智能化管理、企业级应用。 本系统采用SpringBoot+Vue+MyBatis架构,结合MySQL数据库,实现了前后端分离的高效开发模式。SpringBoot框架简化了后端服务的搭建,提供了稳定的RESTful API接口;Vue.js框架构建了响应式前端界面,提升了用户体验;MyBatis作为ORM工具,优化了数据库操作效率。系统功能涵盖用户注册登录、影片信息管理、场次排期、在线选座购票、订单支付、数据统计等模块,支持多角色权限管理,满足影院管理员和普通用户的不同需求。关键词:SpringBoot、Vue.js、MyBatis、MySQL、权

By Ne0inhk