feat: implement safe area insets for Android and add development tooling
- Add @capacitor/status-bar dependency for safe area detection
- Implement SafeAreaPlugin for Android with proper inset calculation
- Create safeAreaInset.js utility for CSS custom property injection
- Update Android manifest and build configuration for plugin
- Integrate safe area handling across Vue components and views
- Update iOS Podfile and Android gradle configurations
- Add commitlint and husky for commit message validation
Technical changes:
- SafeAreaPlugin uses WindowInsets API for Android R+ devices
- Fallback detection for navigation bar and gesture bar heights
- CSS custom properties: --safe-area-inset-{top,bottom,left,right}
- Platform-specific detection (Android WebView only)
- StatusBar plugin integration for top inset calculation
This commit is contained in:
226
src/utils/safeAreaInset.js
Normal file
226
src/utils/safeAreaInset.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Safe Area Inset Injection for Android WebView
|
||||
*
|
||||
* This script injects safe area inset values into CSS environment variables
|
||||
* when running in Android WebView, since Android doesn't natively support
|
||||
* CSS env(safe-area-inset-*) variables like iOS does.
|
||||
*/
|
||||
|
||||
// Check if we're running in Android WebView with Capacitor
|
||||
const isAndroidWebView = () => {
|
||||
// Check if we're on iOS - if so, skip this script entirely
|
||||
const isIOS =
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
|
||||
if (isIOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're on Android
|
||||
const isAndroid = /Android/.test(navigator.userAgent);
|
||||
|
||||
// Check if we have Capacitor (required for Android WebView)
|
||||
const hasCapacitor = window.Capacitor !== undefined;
|
||||
|
||||
// Only run on Android with Capacitor
|
||||
return isAndroid && hasCapacitor;
|
||||
};
|
||||
|
||||
// Wait for Capacitor to be available
|
||||
const waitForCapacitor = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (window.Capacitor) {
|
||||
resolve(window.Capacitor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for Capacitor to be available
|
||||
const checkCapacitor = () => {
|
||||
if (window.Capacitor) {
|
||||
resolve(window.Capacitor);
|
||||
} else {
|
||||
setTimeout(checkCapacitor, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkCapacitor();
|
||||
});
|
||||
};
|
||||
|
||||
// Inject safe area inset values into CSS custom properties
|
||||
const injectSafeAreaInsets = async () => {
|
||||
try {
|
||||
// Wait for Capacitor to be available
|
||||
const Capacitor = await waitForCapacitor();
|
||||
|
||||
// Try to get safe area insets using StatusBar plugin (which is already available)
|
||||
|
||||
let top = 0,
|
||||
bottom = 0,
|
||||
left = 0,
|
||||
right = 0;
|
||||
|
||||
try {
|
||||
// Use StatusBar plugin to get status bar height
|
||||
if (Capacitor.Plugins.StatusBar) {
|
||||
const statusBarInfo = await Capacitor.Plugins.StatusBar.getInfo();
|
||||
// Status bar height is typically the top safe area inset
|
||||
top = statusBarInfo.overlays ? 0 : statusBarInfo.height || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
// Status bar info not available, will use fallback
|
||||
}
|
||||
|
||||
// Detect navigation bar and gesture bar heights
|
||||
const detectNavigationBar = () => {
|
||||
const screenHeight = window.screen.height;
|
||||
const screenWidth = window.screen.width;
|
||||
const windowHeight = window.innerHeight;
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
// Calculate navigation bar height
|
||||
let navBarHeight = 0;
|
||||
|
||||
// Method 1: Direct comparison (most reliable)
|
||||
if (windowHeight < screenHeight) {
|
||||
navBarHeight = screenHeight - windowHeight;
|
||||
}
|
||||
|
||||
// Method 2: Check for gesture navigation indicators
|
||||
if (navBarHeight === 0) {
|
||||
// Look for common gesture navigation patterns
|
||||
const isTallDevice = screenHeight > 2000;
|
||||
const isModernDevice = screenHeight > 1800;
|
||||
const hasHighDensity = devicePixelRatio >= 2.5;
|
||||
|
||||
if (isTallDevice && hasHighDensity) {
|
||||
// Modern gesture-based device
|
||||
navBarHeight = 12; // Typical gesture bar height
|
||||
} else if (isModernDevice) {
|
||||
// Modern device with traditional navigation
|
||||
navBarHeight = 48; // Traditional navigation bar height
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check visual viewport (more accurate for WebView)
|
||||
if (navBarHeight === 0) {
|
||||
if (window.visualViewport) {
|
||||
const visualHeight = window.visualViewport.height;
|
||||
|
||||
if (visualHeight < windowHeight) {
|
||||
navBarHeight = windowHeight - visualHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Device-specific estimation based on screen dimensions
|
||||
if (navBarHeight === 0) {
|
||||
// Common Android navigation bar heights in pixels
|
||||
const commonNavBarHeights = {
|
||||
"1080x2400": 48, // Common 1080p devices
|
||||
"1440x3200": 64, // QHD devices
|
||||
"720x1600": 32, // HD devices
|
||||
};
|
||||
|
||||
const resolution = `${screenWidth}x${screenHeight}`;
|
||||
const estimatedHeight = commonNavBarHeights[resolution];
|
||||
|
||||
if (estimatedHeight) {
|
||||
navBarHeight = estimatedHeight;
|
||||
} else {
|
||||
// Fallback: estimate based on screen height
|
||||
navBarHeight = screenHeight > 2000 ? 48 : 32;
|
||||
}
|
||||
}
|
||||
|
||||
return navBarHeight;
|
||||
};
|
||||
|
||||
// Get navigation bar height
|
||||
bottom = detectNavigationBar();
|
||||
|
||||
// If we still don't have a top value, estimate it
|
||||
if (top === 0) {
|
||||
const screenHeight = window.screen.height;
|
||||
// Common status bar heights: 24dp (48px) for most devices, 32dp (64px) for some
|
||||
top = screenHeight > 1920 ? 64 : 48;
|
||||
}
|
||||
|
||||
// Left/right safe areas are rare on Android
|
||||
left = 0;
|
||||
right = 0;
|
||||
|
||||
// Create CSS custom properties
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
:root {
|
||||
--safe-area-inset-top: ${top}px;
|
||||
--safe-area-inset-bottom: ${bottom}px;
|
||||
--safe-area-inset-left: ${left}px;
|
||||
--safe-area-inset-right: ${right}px;
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject the style into the document head
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Also set CSS environment variables if supported
|
||||
if (CSS.supports("env(safe-area-inset-top)")) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-top",
|
||||
`${top}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-bottom",
|
||||
`${bottom}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-left",
|
||||
`${left}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-right",
|
||||
`${right}px`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error injecting safe area insets, will use fallback values
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
const initializeSafeArea = () => {
|
||||
// Check if we should run this script at all
|
||||
if (!isAndroidWebView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a small delay to ensure WebView is fully initialized
|
||||
setTimeout(() => {
|
||||
injectSafeAreaInsets();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeSafeArea);
|
||||
} else {
|
||||
initializeSafeArea();
|
||||
}
|
||||
|
||||
// Re-inject on orientation change (only on Android)
|
||||
window.addEventListener("orientationchange", () => {
|
||||
if (isAndroidWebView()) {
|
||||
setTimeout(() => injectSafeAreaInsets(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-inject on resize (only on Android)
|
||||
window.addEventListener("resize", () => {
|
||||
if (isAndroidWebView()) {
|
||||
setTimeout(() => injectSafeAreaInsets(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
export { injectSafeAreaInsets, isAndroidWebView };
|
||||
Reference in New Issue
Block a user