前阵子用 uniapp 做了一款扫码点餐应用,开发的时候用的安卓测试机,感觉一切正常。结果一发布到 iOS 上,用户反馈底部按钮被 Home 条挡住了一半,点也点不中。我赶紧拿了台 iPhone 14 Pro 模拟器看了一眼,确实惨不忍睹。最讽刺的是我用的还是系统自带 tabbar,怎么也会踩到安全区域?后来查了下,是因为自定义样式的 tabbar 没有适配底部安全区。
仔细研究后发现,uniapp 这种多端框架,想一套代码吃遍所有平台,安全区域这块绝对绕不开。今天就拿我自己平时的适配方案从原理到代码完整写一遍,希望能帮你少走几个我踩过的坑。
先从“安全区域”这词说起
手机屏幕现在不再是完整的矩形。iPhone 有刘海和底部 Home 条,安卓机有挖孔屏、水滴屏。系统为了保证内容不被这些结构遮挡,专门划分出一个“安全区域”,意思是你的界面元素最好放在这个区域里,否则就可能被挡住。
在 CSS 里,我们可以用 env(safe-area-inset-bottom) 这样的变量拿到安全区域的值,前提是页面的 viewport 要设置成 viewport-fit=cover。而在 uniapp 里,如果你只跑 H5,那直接在 index.html 里改 meta 标签就行。
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
但等你跑到小程序或者 App 端时,这条路就不一定那么顺畅了。小程序有它自己的环境,App 端又要看 WebView 的实现。我自己的感受是:千万别只依赖 CSS env,最好用 JS 把具体数值算出来,再手动加给元素。
官方给的属性:safeAreaInsets
uniapp 接口里有一个很关键的参数:safeAreaInsets。你可以通过 uni.getSystemInfoSync() 拿到它:
const info = uni.getSystemInfoSync()
console.log(info.safeAreaInsets)
在 iPhone 14 Pro 上,我打印出来的结果是这样的:
{left: 0, right: 0, top: 47, bottom: 34}
在普通国产安卓机上,结果通常是:
{left: 0, right: 0, top: 0, bottom: 0}
这就很直观了:bottom 就是底部 Home 条的高度,top 是刘海区域的高度。left 和 right 在横屏时可能会有值。
底部按钮适配:最常遇到的需求
很多页面底部都会放一个固定定位的“确认支付”或“提交订单”按钮,如果不处理,iPhone 的 Home 条会直接盖住按钮文字。
我推荐的做法是:在页面里先把安全区域的 bottom 值存下来,然后给底部按钮加一个动态 padding。
完整代码大概长这样(Vue3 组合式写法):
<template>
<view class="page">
<view class="content">
<!-- 页面主要内容 -->
</view>
<view class="footer" :style="{ paddingBottom: bottomSafe + 'px' }">
<button class="submit-btn">确认支付</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const bottomSafe = ref(0)
onLoad(() => {
// 低版本安卓机器可能没有 safeAreaInsets,所以给个默认值
const { safeAreaInsets } = uni.getSystemInfoSync()
bottomSafe.value = (safeAreaInsets && safeAreaInsets.bottom) || 0
})
</script>
然后在样式文件里写上常规的底部高度就行:
.footer {
width: 100%;
background: #fff;
box-sizing: border-box;
padding-bottom: 12px; /* 这个 12px 是给原本元素的间距,动态 padding 会额外加上 */
}
.submit-btn {
width: 90%;
margin: 0 auto;
height: 48px;
background: #07c160;
color: white;
border-radius: 24px;
line-height: 48px;
text-align: center;
font-size: 16px;
border: none;
}
这段代码在 App 端和小程序端都可以工作。为什么不用 CSS 的 env()?因为小程序端虽然有支持,但安卓一些低版本 WebView 表现不稳定,而且 App 端如果你使用的是 nvue 页面,处理方式又不一样。用 JS 赋值是最简单粗暴且统一的方式。
自定义导航栏:顶部刘海适配
当你的页面使用自定义导航栏时,就需要考虑顶部状态栏的高度。最典型的场景是做一个沉浸式大图页面,标题栏浮在图片上。
获取顶部高度也有专用的属性:statusBarHeight。
const info = uni.getSystemInfoSync()
console.log(info.statusBarHeight)
在 iPhone 上,我习惯把状态栏高度和标题栏拼在一起,动态设置导航栏的高度:
<template>
<view class="custom-nav" :style="{ height: navHeight + 'px' }">
<view class="nav-title">商品详情</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const navHeight = ref(44)
onLoad(() => {
const { statusBarHeight } = uni.getSystemInfoSync()
// 48 是导航栏自身的高度,你可以按设计稿调整
navHeight.value = (statusBarHeight || 0) + 48
})
</script>
这样在 iPhone 上导航栏高度大约是 47 + 48 = 95px,在普通安卓上大约是 24 + 48 = 72px。不同机型有不同高度,但至少不会被状态栏遮挡。
横屏适配:别忘了左右安全区
有些项目需要支持横屏,比如扫码枪配对、视频横播放。一旦转横屏,安全区域就跑到屏幕左右两侧了。
实测在 iPhone 横屏时,safeAreaInsets 可能是这样:
{left: 47, right: 47, top: 0, bottom: 21}
所以如果你有一个左右滑动的设计,最好根据这个值给容器加左右 padding。
const { safeAreaInsets } = uni.getSystemInfoSync()
const horizontalPadding = Math.max(
safeAreaInsets?.left || 0,
safeAreaInsets?.right || 0
)
然后把它用到滚动区域里。注意在横屏下,底部那条 Home 条仍然存在,bottom 也会有一个小值,所以底部按钮的适配依然不能忘。
再说说 CSS env 的正确使用姿势
虽然我前面说要靠 JS,但 CSS 方案在某些轻量场景下还是很方便。比如你想让一个整页背景图全屏铺开,不被刘海遮挡,同时保持内容在安全区内,可以这样写:
.banner {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
但这里有一个前提:页面所在的环境必须支持这两个 CSS 变量。H5 端只要你加了 viewport-fit=cover 就行。小程序端在微信开发者工具里也能识别。App 端则需要 WebView 内核完整支持。反正我试过几台安卓老机器,样式根本没生效。
所以我的最终建议是:能用 JS 拿数值就用 JS,CSS env 只用来做“增强效果”,不要依赖它作为唯一方案。
TabBar 适配:最容易翻车的地方
再用 tabbar 举例。uniapp 自带的 tabbar 在 iOS 上会自动处理安全区,但如果用自定义 tabbar(custom-tab-bar),你就得自己动手。
自定义 tabbar 的结构一般就这样:
<view class="tabbar">
<view class="tabbar-item">首页</view>
<view class="tabbar-item">订单</view>
<view class="tabbar-item">我的</view>
</view>
你可以在 tabbar 这个组件里也加上安全区的动态 padding。但要知道,自定义 tabbar 最后会单独编译成一个组件,然后插入到页面里。在 App 端和微信小程序端,这个组件同样可以调用 uni.getSystemInfoSync()。
在 tabbar 组件中加一个计算属性:
computed: {
tabbarPadding() {
return (this.safeAreaInsets && this.safeAreaInsets.bottom) + 'px'
}
}
然后在模板里给整个 tabbar 根节点绑定样式:
<view class="tabbar" :style="{ paddingBottom: tabbarPadding }">
这个方法虽然麻烦,但起码能保证不管在哪台手机上,都不会出现 Home 条把 tabbar 文字顶掉的情况。
实践中的几个小教训
最后分享几个我踩过的坑。
第一,safeAreaInsets 在某些安卓机器上永远都是 0,不要因此以为没有安全区。很多安卓机的挖孔屏比较靠近屏幕顶部,但系统并没有上报 safeArea,这时就需要用 statusBarHeight 来辅助判断。
第二,在 Vue3 的 uniapp 项目里,onLoad 需要从 @dcloudio/uni-app 里导入,不是全局自动注入。我第一次写的时候没注意,结果 onLoad 直接未定义。
第三,如果某个页面同时用了自定义导航栏和底部按钮,别把 top 和 bottom 混在一起。最好在 onLoad 里一次性取好 statusBarHeight 和 safeAreaInsets,存成两个变量,后面各用各的。
第四,App 端 nvue 页面的安全区处理不太一样。因为 nvue 用的是原生渲染,safeAreaInsets 仍然可用,但 CSS 的 env 在 nvue 里不一定支持。所以 nvue 页面更得用 JS 动态设置。
写在最后
安全区域适配本身不难,难的是不同端各自为政。uniapp 已经帮我们做了很多封装,但这些东西还是得自己懂。我这点经验分享出来,如果你正准备做自定义 tabbar 或者底部固定按钮,希望可以省下一些百度的时间。
最后记住一句话:先拿 safeAreaInsets,再写样式;能动态就动态,别硬编码 44px 或者 34px。这样才能让你的 app 在各种奇怪屏幕面前挺住。

