safe-area-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSafe Area Handling in Capacitor
Capacitor应用中的安全区域处理
Handle iPhone notch, Dynamic Island, home indicator, and Android cutouts properly.
正确处理iPhone刘海屏、Dynamic Island、Home指示器及Android挖孔屏的布局适配问题。
When to Use This Skill
何时使用此技能
- User has layout issues on notched devices
- User asks about safe areas
- User sees content under the notch
- User needs fullscreen layout
- Content is hidden by home indicator
- 用户在带刘海/挖孔的设备上遇到布局问题
- 用户询问安全区域相关问题
- 用户发现内容被刘海遮挡
- 用户需要全屏布局方案
- 内容被Home指示器遮挡
Understanding Safe Areas
理解安全区域
What Are Safe Areas?
什么是安全区域?
Safe areas are the regions of the screen not obscured by:
- iPhone: Notch, Dynamic Island, home indicator, rounded corners
- Android: Camera cutouts, navigation gestures, display cutouts
安全区域是指屏幕上未被以下元素遮挡的区域:
- iPhone:刘海屏、Dynamic Island、Home指示器、圆角
- Android:摄像头挖孔、导航手势区域、显示挖孔
Safe Area Insets
安全区域内边距
| Inset | Description |
|---|---|
| Notch/Dynamic Island/status bar |
| Home indicator/navigation bar |
| Left edge (landscape) |
| Right edge (landscape) |
| 内边距 | 描述 |
|---|---|
| 刘海/Dynamic Island/状态栏区域 |
| Home指示器/导航栏区域 |
| 左侧边缘(横屏模式) |
| 右侧边缘(横屏模式) |
CSS Solution
CSS解决方案
Enable Viewport Coverage
启用视口全覆盖
html
<!-- index.html -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>Important: is required to access safe area insets.
viewport-fit=coverhtml
<!-- index.html -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>重要提示:必须设置才能访问安全区域内边距。
viewport-fit=coverUsing CSS Environment Variables
使用CSS环境变量
css
/* Basic usage */
.header {
padding-top: env(safe-area-inset-top);
}
.footer {
padding-bottom: env(safe-area-inset-bottom);
}
/* With fallback */
.header {
padding-top: env(safe-area-inset-top, 20px);
}
/* Combined with other padding */
.content {
padding-top: calc(env(safe-area-inset-top) + 16px);
padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
}css
/* 基础用法 */
.header {
padding-top: env(safe-area-inset-top);
}
.footer {
padding-bottom: env(safe-area-inset-bottom);
}
/* 带回退值的用法 */
.header {
padding-top: env(safe-area-inset-top, 20px);
}
/* 与其他内边距组合使用 */
.content {
padding-top: calc(env(safe-area-inset-top) + 16px);
padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
}Full Page Layout
整页布局方案
css
/* App container */
.app {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
}
/* Header respects notch */
.header {
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
background: #fff;
}
/* Scrollable content */
.content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
/* Footer respects home indicator */
.footer {
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
background: #fff;
}css
/* 应用容器 */
.app {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
}
/* 适配刘海的头部 */
.header {
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
background: #fff;
}
/* 可滚动内容区 */
.content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
/* 适配Home指示器的底部 */
.footer {
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
background: #fff;
}Tab Bar with Safe Area
适配安全区域的标签栏
css
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: #fff;
border-top: 1px solid #eee;
/* Add padding for home indicator */
padding-bottom: env(safe-area-inset-bottom);
}
.tab-bar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
min-height: 49px; /* iOS standard height */
}css
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: #fff;
border-top: 1px solid #eee;
/* 添加Home指示器的内边距 */
padding-bottom: env(safe-area-inset-bottom);
}
.tab-bar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
min-height: 49px; /* iOS标准高度 */
}Full-Bleed Background with Safe Content
全屏背景+安全区域内容
css
.hero {
/* Background extends to edges */
background: linear-gradient(to bottom, #4f46e5, #7c3aed);
padding-top: calc(env(safe-area-inset-top) + 20px);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.hero-content {
/* Content stays in safe area */
max-width: 100%;
}css
.hero {
/* 背景延伸至屏幕边缘 */
background: linear-gradient(to bottom, #4f46e5, #7c3aed);
padding-top: calc(env(safe-area-inset-top) + 20px);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.hero-content {
/* 内容保持在安全区域内 */
max-width: 100%;
}JavaScript Solution
JavaScript解决方案
Reading Safe Area Values
读取安全区域值
typescript
function getSafeAreaInsets() {
const computedStyle = getComputedStyle(document.documentElement);
return {
top: parseInt(computedStyle.getPropertyValue('--sat') || '0'),
bottom: parseInt(computedStyle.getPropertyValue('--sab') || '0'),
left: parseInt(computedStyle.getPropertyValue('--sal') || '0'),
right: parseInt(computedStyle.getPropertyValue('--sar') || '0'),
};
}
// Set CSS custom properties
function setSafeAreaProperties() {
const style = document.documentElement.style;
// Create temporary element to read values
const temp = document.createElement('div');
temp.style.paddingTop = 'env(safe-area-inset-top)';
temp.style.paddingBottom = 'env(safe-area-inset-bottom)';
temp.style.paddingLeft = 'env(safe-area-inset-left)';
temp.style.paddingRight = 'env(safe-area-inset-right)';
document.body.appendChild(temp);
const computed = getComputedStyle(temp);
style.setProperty('--sat', computed.paddingTop);
style.setProperty('--sab', computed.paddingBottom);
style.setProperty('--sal', computed.paddingLeft);
style.setProperty('--sar', computed.paddingRight);
document.body.removeChild(temp);
}
// Update on orientation change
window.addEventListener('orientationchange', () => {
setTimeout(setSafeAreaProperties, 100);
});typescript
function getSafeAreaInsets() {
const computedStyle = getComputedStyle(document.documentElement);
return {
top: parseInt(computedStyle.getPropertyValue('--sat') || '0'),
bottom: parseInt(computedStyle.getPropertyValue('--sab') || '0'),
left: parseInt(computedStyle.getPropertyValue('--sal') || '0'),
right: parseInt(computedStyle.getPropertyValue('--sar') || '0'),
};
}
// 设置CSS自定义属性
function setSafeAreaProperties() {
const style = document.documentElement.style;
// 创建临时元素以读取值
const temp = document.createElement('div');
temp.style.paddingTop = 'env(safe-area-inset-top)';
temp.style.paddingBottom = 'env(safe-area-inset-bottom)';
temp.style.paddingLeft = 'env(safe-area-inset-left)';
temp.style.paddingRight = 'env(safe-area-inset-right)';
document.body.appendChild(temp);
const computed = getComputedStyle(temp);
style.setProperty('--sat', computed.paddingTop);
style.setProperty('--sab', computed.paddingBottom);
style.setProperty('--sal', computed.paddingLeft);
style.setProperty('--sar', computed.paddingRight);
document.body.removeChild(temp);
}
// 屏幕旋转时更新
window.addEventListener('orientationchange', () => {
setTimeout(setSafeAreaProperties, 100);
});React Hook
React Hook
typescript
import { useState, useEffect } from 'react';
interface SafeAreaInsets {
top: number;
bottom: number;
left: number;
right: number;
}
function useSafeArea(): SafeAreaInsets {
const [insets, setInsets] = useState<SafeAreaInsets>({
top: 0,
bottom: 0,
left: 0,
right: 0,
});
useEffect(() => {
function updateInsets() {
const temp = document.createElement('div');
temp.style.cssText = `
position: fixed;
top: 0;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
`;
document.body.appendChild(temp);
const computed = getComputedStyle(temp);
setInsets({
top: parseFloat(computed.paddingTop) || 0,
bottom: parseFloat(computed.paddingBottom) || 0,
left: parseFloat(computed.paddingLeft) || 0,
right: parseFloat(computed.paddingRight) || 0,
});
document.body.removeChild(temp);
}
updateInsets();
window.addEventListener('resize', updateInsets);
window.addEventListener('orientationchange', () => {
setTimeout(updateInsets, 100);
});
return () => {
window.removeEventListener('resize', updateInsets);
};
}, []);
return insets;
}
// Usage
function Header() {
const { top } = useSafeArea();
return (
<header style={{ paddingTop: top }}>
App Header
</header>
);
}typescript
import { useState, useEffect } from 'react';
interface SafeAreaInsets {
top: number;
bottom: number;
left: number;
right: number;
}
function useSafeArea(): SafeAreaInsets {
const [insets, setInsets] = useState<SafeAreaInsets>({
top: 0,
bottom: 0,
left: 0,
right: 0,
});
useEffect(() => {
function updateInsets() {
const temp = document.createElement('div');
temp.style.cssText = `
position: fixed;
top: 0;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
`;
document.body.appendChild(temp);
const computed = getComputedStyle(temp);
setInsets({
top: parseFloat(computed.paddingTop) || 0,
bottom: parseFloat(computed.paddingBottom) || 0,
left: parseFloat(computed.paddingLeft) || 0,
right: parseFloat(computed.paddingRight) || 0,
});
document.body.removeChild(temp);
}
updateInsets();
window.addEventListener('resize', updateInsets);
window.addEventListener('orientationchange', () => {
setTimeout(updateInsets, 100);
});
return () => {
window.removeEventListener('resize', updateInsets);
};
}, []);
return insets;
}
// 使用示例
function Header() {
const { top } = useSafeArea();
return (
<header style={{ paddingTop: top }}>
应用头部
</header>
);
}Vue Composable
Vue 组合式函数
typescript
import { ref, onMounted, onUnmounted } from 'vue';
export function useSafeArea() {
const insets = ref({
top: 0,
bottom: 0,
left: 0,
right: 0,
});
function updateInsets() {
const temp = document.createElement('div');
temp.style.cssText = `
position: fixed;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
`;
document.body.appendChild(temp);
const computed = getComputedStyle(temp);
insets.value = {
top: parseFloat(computed.paddingTop) || 0,
bottom: parseFloat(computed.paddingBottom) || 0,
left: parseFloat(computed.paddingLeft) || 0,
right: parseFloat(computed.paddingRight) || 0,
};
document.body.removeChild(temp);
}
onMounted(() => {
updateInsets();
window.addEventListener('resize', updateInsets);
});
onUnmounted(() => {
window.removeEventListener('resize', updateInsets);
});
return insets;
}typescript
import { ref, onMounted, onUnmounted } from 'vue';
export function useSafeArea() {
const insets = ref({
top: 0,
bottom: 0,
left: 0,
right: 0,
});
function updateInsets() {
const temp = document.createElement('div');
temp.style.cssText = `
position: fixed;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
`;
document.body.appendChild(temp);
const computed = getComputedStyle(temp);
insets.value = {
top: parseFloat(computed.paddingTop) || 0,
bottom: parseFloat(computed.paddingBottom) || 0,
left: parseFloat(computed.paddingLeft) || 0,
right: parseFloat(computed.paddingRight) || 0,
};
document.body.removeChild(temp);
}
onMounted(() => {
updateInsets();
window.addEventListener('resize', updateInsets);
});
onUnmounted(() => {
window.removeEventListener('resize', updateInsets);
});
return insets;
}Native iOS Configuration
iOS原生配置
Status Bar Style
状态栏样式
typescript
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
ios: {
// Content extends behind status bar
contentInset: 'automatic', // or 'always', 'scrollableAxes', 'never'
},
};typescript
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
ios: {
// 内容延伸至状态栏后方
contentInset: 'automatic', // 可选值:'always', 'scrollableAxes', 'never'
},
};Extend Behind Safe Areas
延伸内容至安全区域外
swift
// ios/App/App/AppDelegate.swift
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Extend content to edges
if let window = UIApplication.shared.windows.first {
window.backgroundColor = .clear
}
return true
}
}swift
// ios/App/App/AppDelegate.swift
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 将内容延伸至屏幕边缘
if let window = UIApplication.shared.windows.first {
window.backgroundColor = .clear
}
return true
}
}Info.plist Settings
Info.plist 设置
xml
<!-- ios/App/App/Info.plist -->
<!-- Allow full screen content -->
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<!-- For landscape support -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>xml
<!-- ios/App/App/Info.plist -->
<!-- 允许全屏内容 -->
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<!-- 支持横屏 -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>Native Android Configuration
Android原生配置
Display Cutout Mode
显示挖孔模式
xml
<!-- android/app/src/main/res/values-v28/styles.xml -->
<resources>
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<!-- Extend content into cutout area -->
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
</resources>xml
<!-- android/app/src/main/res/values-v28/styles.xml -->
<resources>
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<!-- 将内容延伸至挖孔区域 -->
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
</resources>Edge-to-Edge Display
全屏显示(边缘到边缘)
kotlin
// android/app/src/main/java/.../MainActivity.kt
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
class MainActivity : BridgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Enable edge-to-edge
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
}
}kotlin
// android/app/src/main/java/.../MainActivity.kt
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
class MainActivity : BridgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 启用边缘到边缘显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
}
}AndroidManifest Configuration
AndroidManifest 配置
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode">
</activity>xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode">
</activity>Capacitor Status Bar Plugin
Capacitor状态栏插件
Installation
安装
bash
bun add @capacitor/status-bar
bunx cap syncbash
bun add @capacitor/status-bar
bunx cap syncUsage
使用
typescript
import { StatusBar, Style } from '@capacitor/status-bar';
// Set status bar style
await StatusBar.setStyle({ style: Style.Dark });
// Set background color (Android)
await StatusBar.setBackgroundColor({ color: '#ffffff' });
// Show/hide status bar
await StatusBar.hide();
await StatusBar.show();
// Overlay mode
await StatusBar.setOverlaysWebView({ overlay: true });typescript
import { StatusBar, Style } from '@capacitor/status-bar';
// 设置状态栏样式
await StatusBar.setStyle({ style: Style.Dark });
// 设置背景色(Android)
await StatusBar.setBackgroundColor({ color: '#ffffff' });
// 显示/隐藏状态栏
await StatusBar.hide();
await StatusBar.show();
// 覆盖模式
await StatusBar.setOverlaysWebView({ overlay: true });Common Issues and Solutions
常见问题与解决方案
Issue: Content Behind Notch
问题:内容被刘海遮挡
Solution: Add viewport-fit and safe area padding
html
<meta name="viewport" content="viewport-fit=cover">css
body {
padding-top: env(safe-area-inset-top);
}解决方案:添加viewport-fit和安全区域内边距
html
<meta name="viewport" content="viewport-fit=cover">css
body {
padding-top: env(safe-area-inset-top);
}Issue: Tab Bar Under Home Indicator
问题:标签栏被Home指示器遮挡
Solution: Add bottom safe area padding
css
.tab-bar {
padding-bottom: env(safe-area-inset-bottom);
}解决方案:添加底部安全区域内边距
css
.tab-bar {
padding-bottom: env(safe-area-inset-bottom);
}Issue: Landscape Layout Broken
问题:横屏布局损坏
Solution: Handle left/right insets
css
.content {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}解决方案:处理左右内边距
css
.content {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}Issue: Keyboard Pushes Content
问题:键盘弹出时内容被顶起
Solution: Use adjustResize and handle insets dynamically
typescript
import { Keyboard } from '@capacitor/keyboard';
Keyboard.addListener('keyboardWillShow', (info) => {
document.body.style.paddingBottom = `${info.keyboardHeight}px`;
});
Keyboard.addListener('keyboardWillHide', () => {
document.body.style.paddingBottom = 'env(safe-area-inset-bottom)';
});解决方案:使用adjustResize并动态处理内边距
typescript
import { Keyboard } from '@capacitor/keyboard';
Keyboard.addListener('keyboardWillShow', (info) => {
document.body.style.paddingBottom = `${info.keyboardHeight}px`;
});
Keyboard.addListener('keyboardWillHide', () => {
document.body.style.paddingBottom = 'env(safe-area-inset-bottom)';
});Issue: Safe Areas Not Working in WebView
问题:WebView中安全区域不生效
Cause: Missing viewport-fit=cover
Solution:
html
<!-- Must be exactly like this -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>原因:缺少viewport-fit=cover设置
解决方案:
html
<!-- 必须严格按照此格式设置 -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>Testing Safe Areas
测试安全区域
iOS Simulator
iOS模拟器
- Use iPhone with notch (iPhone 14 Pro, etc.)
- Test both portrait and landscape
- Test with keyboard visible
- 使用带刘海的iPhone设备(如iPhone 14 Pro等)
- 测试竖屏和横屏模式
- 测试键盘弹出时的布局
Android Emulator
Android模拟器
- Create emulator with camera cutout
- Test navigation gesture mode
- Test 3-button navigation mode
- 创建带摄像头挖孔的模拟器
- 测试导航手势模式
- 测试三键导航模式
Preview Different Devices
预览不同设备的安全区域
css
/* Debug mode - visualize safe areas */
.debug-safe-areas::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
height: env(safe-area-inset-top);
background: rgba(255, 0, 0, 0.3);
z-index: 9999;
pointer-events: none;
}
.debug-safe-areas::after {
content: '';
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: env(safe-area-inset-bottom);
background: rgba(0, 0, 255, 0.3);
z-index: 9999;
pointer-events: none;
}css
/* 调试模式 - 可视化安全区域 */
.debug-safe-areas::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
height: env(safe-area-inset-top);
background: rgba(255, 0, 0, 0.3);
z-index: 9999;
pointer-events: none;
}
.debug-safe-areas::after {
content: '';
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: env(safe-area-inset-bottom);
background: rgba(0, 0, 255, 0.3);
z-index: 9999;
pointer-events: none;
}Resources
参考资源
- Apple Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines/layout
- Android Display Cutouts: https://developer.android.com/develop/ui/views/layout/display-cutout
- CSS env() specification: https://drafts.csswg.org/css-env-1/
- Capacitor Status Bar: https://capacitorjs.com/docs/apis/status-bar
- Apple人机界面指南:https://developer.apple.com/design/human-interface-guidelines/layout
- Android显示挖孔文档:https://developer.android.com/develop/ui/views/layout/display-cutout
- CSS env()规范:https://drafts.csswg.org/css-env-1/
- Capacitor状态栏文档:https://capacitorjs.com/docs/apis/status-bar