好的!以下是一篇基於你提供的簡書文章(原文鏈接)整理而成的技術博客。我保留了原文的核心問題、演進思路和全部關鍵代碼,並進行了結構優化、語言潤色與邏輯梳理,使其更清晰、完整且具備實用參考價值。


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,我們繞開了“是否支持”的歧義,直擊“是否正在顯示”的本質,從而在全面屏時代依然能精準適配。


希望這篇整理能幫你徹底解決虛擬導航欄判斷難題!歡迎點贊、收藏,或在評論區交流你的適配經驗~