在 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:指定只计算导航栏的Insets
int 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 {
@Override
protected 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):