好的!以下是一篇基於你提供的簡書文章(原文鏈接)整理而成的技術博客。我保留了原文的核心問題、演進思路和全部關鍵代碼,並進行了結構優化、語言潤色與邏輯梳理,使其更清晰、完整且具備實用參考價值。
Android 判斷虛擬導航欄是否真實存在:從失效的老方法到可靠的 View 檢測方案
作者:Qwen
參考來源:簡書《Android 判斷虛擬導航欄是否存在》
在 Android 開發中,適配全面屏、處理屏幕安全區域時,常常需要判斷設備是否實際顯示了虛擬導航欄(NavigationBar)。然而,隨着全面屏手勢的普及,傳統的判斷方法紛紛失效。本文將帶你回顧經典方案為何失靈,並提供兩種可靠、通用的新解決方案。
一、傳統判斷方法及其侷限性
在過去,開發者普遍採用以下幾種方式判斷虛擬導航欄是否存在:
方法 1:檢查系統資源 navigation_bar_height
Resources res = activity.getResources();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
return res.getDimensionPixelSize(resourceId) > 0;
}
❌ 問題:幾乎所有現代 Android 設備(包括使用手勢的)都定義了該資源,即使導航欄被隱藏。因此此方法永遠返回 true。
方法 2:讀取系統布爾值 config_showNavigationBar
int id = resources.getIdentifier("config_showNavigationBar", "bool", "android");
return id > 0 && resources.getBoolean(id);
❌ 問題:該值表示“系統是否支持虛擬導航欄”,而非“當前是否正在顯示”。全面屏手機通常同時支持手勢和導航欄,此值恆為 true。
方法 3:比較屏幕真實尺寸與可用尺寸
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Display display = context.getWindowManager().getDefaultDisplay();
Point size = new Point(); // 應用可用區域
Point realSize = new Point(); // 物理屏幕尺寸
display.getSize(size);
display.getRealSize(realSize);
return realSize.y != size.y; // 若不等,則有系統欄(狀態欄或導航欄)
}
⚠️ 問題:此方法只能判斷是否有系統 UI 佔用空間(可能是狀態欄或導航欄),無法區分兩者。更重要的是,當導航欄被隱藏時,size 會等於 realSize,導致誤判為“不存在導航欄”——但這只是暫時隱藏,不代表設備沒有集成導航欄功能。
根本原因:全面屏打破了“物理鍵 vs 虛擬鍵”的二元對立
過去:有物理按鍵 → 無虛擬導航欄;無物理按鍵 → 必有虛擬導航欄。
現在:無物理按鍵 + 可切換“手勢”或“虛擬導航欄”。
因此,所有基於“系統是否定義了導航欄”的間接判斷都不可靠。我們需要一個直接證據:導航欄 View 是否正在繪製?
二、新方案一:通過 DecorView 查找 NavigationBarBackground
虛擬導航欄在 Android 5.0+ 是 DecorView 的子 View,且其 ID 對應的資源名為 "navigationBarBackground"。
✅ 優點:直接檢測 View 是否存在,結果準確。
⚠️ 注意:必須在 View 繪製完成後調用(如onWindowFocusChanged)。
public static boolean isNavigationBarExist(@NonNull Activity activity) {
final String NAVIGATION_VIEW_NAME = "navigationBarBackground";
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
if (decorView == null) return false;
for (int i = 0; i < decorView.getChildCount(); i++) {
View child = decorView.getChildAt(i);
if (child.getId() != View.NO_ID) {
try {
String resourceName = activity.getResources()
.getResourceEntryName(child.getId());
if (NAVIGATION_VIEW_NAME.equals(resourceName)) {
return true;
}
} catch (Resources.NotFoundException e) {
// 忽略無效 ID
}
}
}
return false;
}
使用示例:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
boolean hasNavBar = isNavigationBarExist(this);
Log.d("NavBar", "Exists: " + hasNavBar);
}
}
三、新方案二:利用 WindowInsets 監聽底部內邊距
通過監聽 WindowInsets,我們可以實時獲取系統 UI(包括導航欄)佔用的空間,並與已知的導航欄高度比較。
✅ 優點:可動態響應導航欄的顯示/隱藏(如用户切換手勢/導航欄)。
✅ 適用場景:需要實時適配佈局的頁面。
步驟 1:獲取標準導航欄高度
public static int getNavigationBarHeight(Context context) {
Resources resources = context.getResources();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
return resourceId > 0 ? resources.getDimensionPixelSize(resourceId) : 0;
}
步驟 2:設置 OnApplyWindowInsetsListener
public interface OnNavigationStateListener {
void onNavigationState(boolean isShowing, int bottomInset);
}
public static void listenNavigationBar(Activity activity, OnNavigationStateListener listener) {
if (activity == null || listener == null) return;
final int navBarHeight = getNavigationBarHeight(activity);
View rootView = activity.getWindow().getDecorView();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
rootView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
int bottom = insets.getSystemWindowInsetBottom();
boolean isShowing = (bottom == navBarHeight);
listener.onNavigationState(isShowing, bottom);
return insets;
}
});
}
}
使用示例:
listenNavigationBar(this, new OnNavigationStateListener() {
@Override
public void onNavigationState(boolean isShowing, int bottomInset) {
Log.d("NavBar", "Showing: " + isShowing + ", Bottom: " + bottomInset);
// 根據 isShowing 調整佈局 padding
}
});
💡 提示:此方法在導航欄動態呼出/隱藏時也能正確響應,適合做沉浸式體驗。
四、為什麼不推薦廠商定製方案?
部分廠商(如 vivo、小米)提供了特定的系統設置項來判斷是否啓用手勢:
// 示例:vivo 手勢判斷(不推薦通用使用)
int val = Settings.Secure.getInt(context.getContentResolver(), "navigation_gesture_on", 0);
boolean isGesture = (val != 0);
缺點:
- ❌ 無通用性:每個廠商字段名不同,甚至同一廠商不同機型也不同。
- ❌ 權限限制:部分字段需系統簽名才能讀取。
- ❌ 無法覆蓋所有場景:如可臨時呼出的導航欄。
因此,優先使用上述兩種通用方案。
五、總結與建議
|
方案
|
適用場景
|
優點
|
缺點
|
|
DecorView 檢測 |
靜態判斷(如初始化時)
|
準確、通用
|
需在 View 繪製後調用
|
|
WindowInsets 監聽 |
動態適配(如全屏視頻)
|
實時響應、支持隱藏/顯示
|
需 Android 4.4+
|
|
廠商定製
|
特定品牌深度適配
|
可能更精確
|
不通用、維護成本高
|
✅ 推薦做法:
- 初始化時用 方案一 判斷設備是否具備顯示導航欄的能力;
- 關鍵頁面(如播放器、遊戲)用 方案二 動態監聽,確保 UI 不被遮擋。
通過直接檢測 View 或系統 insets,我們繞開了“是否支持”的歧義,直擊“是否正在顯示”的本質,從而在全面屏時代依然能精準適配。
希望這篇整理能幫你徹底解決虛擬導航欄判斷難題!歡迎點贊、收藏,或在評論區交流你的適配經驗~