在 Android 中判定底部導航欄是否顯示時,核心痛點是?區分 “導航欄的底部 Insets” 和 “軟鍵盤彈出的底部 Insets”—— 兩者都會導致?getSystemWindowInsetBottom()
?返回非零值,直接判斷會誤將鍵盤彈出當成導航欄顯示。以下是基于?WindowInsets 類型區分?的精準解決方案,兼容不同 Android 版本和場景。
核心原理:通過 Insets 類型過濾鍵盤
Android 的?WindowInsets
?會標記不同來源的 “插入區域”(如導航欄、狀態欄、軟鍵盤),通過?WindowInsetsCompat.Type
?可精準過濾出?僅由導航欄貢獻的底部 Insets,從而排除鍵盤干擾。
關鍵類型說明:
Insets 類型 | 含義 | 需排除 / 保留 |
---|---|---|
Type.NAVIGATION_BARS | 系統導航欄(底部 / 側邊) | 保留(目標判斷對象) |
Type.IME | 軟鍵盤(Input Method Editor) | 排除(干擾項) |
Type.STATUS_BARS | 狀態欄(頂部) | 排除(與底部無關) |
方案實現:兼容高低版本的工具類
以下工具類支持?Android 14(API 34)及以下版本,通過?WindowInsetsCompat
?統一處理 Insets 類型,精準判斷導航欄可見性并獲取高度。
import android.view.View;
import androidx.core.view.WindowInsetsCompat;/*** 精準判斷底部導航欄是否顯示(排除軟鍵盤干擾)*/
public class NavigationBarChecker {/*** 判定底部導航欄當前是否可見(排除鍵盤)* @param rootView 頁面根布局(如 Activity 的 contentView、Fragment 的根View)* @return true:導航欄顯示;false:導航欄隱藏或當前是鍵盤彈出*/public static boolean isNavigationBarVisible(View rootView) {if (rootView == null) {return false;}// 1. 獲取根View的WindowInsets(包含所有插入區域信息)WindowInsetsCompat insetsCompat = ViewCompat.getRootWindowInsets(rootView);if (insetsCompat == null) {return false; // 極端情況(如View未附著到窗口),返回隱藏}// 2. 關鍵:僅獲取“導航欄”貢獻的底部Insets(排除鍵盤、狀態欄等)// Type.NAVIGATION_BARS:指定只計算導航欄的Insetsint navBarBottomInset = insetsCompat.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;// 3. 底部Insets > 0 說明導航欄在底部顯示(若為側邊導航欄,bottom會是0,需額外判斷left/right)return navBarBottomInset > 0;}/*** 獲取底部導航欄的真實高度(排除鍵盤干擾)* @param rootView 頁面根布局* @return 導航欄高度(px);0:導航欄隱藏*/public static int getNavigationBarHeight(View rootView) {if (rootView == null) {return 0;}WindowInsetsCompat insetsCompat = ViewCompat.getRootWindowInsets(rootView);if (insetsCompat == null) {return 0;}// 同樣只取導航欄的底部Insets,即為導航欄高度return insetsCompat.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;}/*** (擴展)判斷是否為軟鍵盤彈出狀態(輔助驗證)* @param rootView 頁面根布局* @return true:鍵盤顯示;false:鍵盤隱藏*/public static boolean isKeyboardVisible(View rootView) {if (rootView == null) {return false;}WindowInsetsCompat insetsCompat = ViewCompat.getRootWindowInsets(rootView);if (insetsCompat == null) {return false;}// 僅判斷“鍵盤”貢獻的底部Insets:>0 說明鍵盤彈出//對core 版本有要求,太低找不到//dependencies {//implementation 'androidx.core:core:1.5.0'//}int keyboardBottomInset = insetsCompat.getInsets(WindowInsetsCompat.Type.ime()).bottom;return keyboardBottomInset > 0;}
}
關鍵細節說明
1. 為什么必須用?ViewCompat.getRootWindowInsets()
?
- 避免直接調用?
rootView.getRootWindowInsets()
:該方法在 API 23(Android 6.0)以上才可用,ViewCompat
?會自動兼容低版本(API 14+),無需額外版本判斷。 - 確保獲取的是 “根 View 的 Insets”:只有根布局(如?
setContentView
?傳入的 View)能拿到完整的系統 Insets,子 View 可能因布局嵌套導致 Insets 被截斷。
2. 如何處理 “側邊導航欄”(如平板橫屏)?
部分設備(平板、折疊屏)在橫屏時會將導航欄放在左側 / 右側,此時?bottom
?Insets 為 0,需額外判斷?left
?或?right
:
// 擴展:判斷任意位置的導航欄是否可見(含側邊)
public static boolean isAnyNavigationBarVisible(View rootView) {if (rootView == null) return false;WindowInsetsCompat insetsCompat = ViewCompat.getRootWindowInsets(rootView);if (insetsCompat == null) return false;WindowInsetsCompat.Insets navInsets = insetsCompat.getInsets(WindowInsetsCompat.Type.navigationBars());// 左/右/下 任意一個方向有Insets,說明導航欄可見return navInsets.left > 0 || navInsets.right > 0 || navInsets.bottom > 0;
}
3. 兼容 Android 14(API 34)的新變化
Android 14 新增了?WindowInsets.Type.systemBars()
(包含狀態欄 + 導航欄),但?Type.navigationBars()
?仍完全兼容,無需修改代碼 ——WindowInsetsCompat
?已內部適配新 API,保證低版本行為一致。
使用示例(在 Activity 中)
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 獲取頁面根布局(必須是setContentView的根View)View rootView = findViewById(android.R.id.content); // 通用獲取根View的方式// 1. 監聽導航欄可見性變化(如鍵盤彈出/收起、旋轉屏幕時)ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {// 判斷導航欄是否顯示(排除鍵盤)boolean isNavVisible = NavigationBarChecker.isNavigationBarVisible(rootView);// 獲取導航欄高度int navHeight = NavigationBarChecker.getNavigationBarHeight(rootView);// 判斷鍵盤是否顯示(輔助)boolean isKeyboard = NavigationBarChecker.isKeyboardVisible(rootView);// 業務邏輯:如更新UI、調整布局Log.d("NavChecker", "導航欄可見:" + isNavVisible + ",高度:" + navHeight + "px,鍵盤可見:" + isKeyboard);return insets; // 必須返回Insets,否則后續監聽會失效});// 2. 主動觸發一次判斷(如頁面初始化時)boolean initNavVisible = NavigationBarChecker.isNavigationBarVisible(rootView);int initNavHeight = NavigationBarChecker.getNavigationBarHeight(rootView);}
}
常見問題排查
返回值始終為 0?
- 檢查?
rootView
?是否為頁面根布局(如用?findViewById(android.R.id.content)
?替代子 View)。 - 確保布局未設置?
fitsSystemWindows="true"
:該屬性會讓 View 消費 Insets,導致?getInsets()
?返回 0(如需使用,需在根 View 的父布局設置)。
- 檢查?
鍵盤彈出時誤判為導航欄?
- 確認代碼中使用?
WindowInsetsCompat.Type.navigationBars()
?而非?Type.systemBars()
?或直接?getSystemWindowInsetBottom()
—— 后者會包含鍵盤 Insets。
- 確認代碼中使用?
低版本(API < 21)不生效?
- Android 5.0(API 21)以下無官方 Insets API,若需兼容,可通過?反射獲取系統資源?間接判斷(但精度較低,建議最低兼容到 API 21):
// 兼容API < 21:通過系統資源判斷導航欄是否存在(無法實時判斷顯示/隱藏) public static boolean hasNavigationBar(Context context) {Resources res = context.getResources();int resourceId = res.getIdentifier("config_showNavigationBar", "bool", "android");if (resourceId > 0) {return res.getBoolean(resourceId);}return false; // 無法判斷時默認返回false }
- Android 5.0(API 21)以下無官方 Insets API,若需兼容,可通過?反射獲取系統資源?間接判斷(但精度較低,建議最低兼容到 API 21):