41、Material Design之BottomNavigationView:底部导航、Badge、与ViewPager联动
底部导航栏,几乎是每个App的标配。你想想看,微信、支付宝、淘宝,哪个不是底部四个tab?用户早就习惯了这种操作方式。今天我们就来聊聊Android里最正统的底部导航实现——BottomNavigationView。
说实话,我刚入行那会儿,底部导航还是用RadioGroup加Fragment硬拼出来的。后来Google出了BottomNavigationView,我才发现原来官方已经帮我们封装好了。嗯,这玩意儿虽然简单,但坑也不少。今天我把这些年踩过的坑、积累的经验,一次性讲清楚。
1. 基础配置:三步搞定底部导航
先看最基础的使用方式。说白了,就是三步:定义菜单、添加控件、绑定事件。
第一步:创建菜单资源文件
在res/menu/目录下新建bottom_nav_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_home"
android:title="首页" />
<item
android:id="@+id/nav_discover"
android:icon="@drawable/ic_discover"
android:title="发现" />
<item
android:id="@+id/nav_message"
android:icon="@drawable/ic_message"
android:title="消息" />
<item
android:id="@+id/nav_profile"
android:icon="@drawable/ic_profile"
android:title="我的" />
</menu>
第二步:布局文件中添加BottomNavigationView
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="@menu/bottom_nav_menu"
app:itemIconTint="@color/bottom_nav_color"
app:itemTextColor="@color/bottom_nav_color" />
这里有个小细节——itemIconTint和itemTextColor。我建议你用一个selector颜色资源,选中状态和未选中状态分开设置,效果会好很多。
第三步:绑定监听事件
BottomNavigationView bottomNavigation = findViewById(R.id.bottom_navigation);
bottomNavigation.setOnItemSelectedListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.nav_home) {
// 切换到首页
return true;
} else if (itemId == R.id.nav_discover) {
// 切换到发现
return true;
}
// ... 其他tab
return false;
});
setOnItemSelectedListener而不是setOnNavigationItemSelectedListener。前者是新的API,更简洁,而且返回boolean值来控制是否选中。如果你返回false,点击就不会切换,这个特性在某些场景下很有用。
2. Badge:给图标加个小红点
底部导航不带Badge,总觉得少了点什么。比如消息tab上显示未读条数,就是典型的Badge场景。
Google在Material Design Components 1.1.0之后,正式支持了Badge。用法很简单:
// 获取或创建Badge
BadgeDrawable badge = bottomNavigation.getOrCreateBadge(R.id.nav_message);
badge.setVisible(true);
badge.setNumber(99); // 显示数字
// badge.setBadgeGravity(BadgeDrawable.TOP_END); // 调整位置
如果你想显示一个小红点(不显示数字),可以这样:
badge.setNumber(0); // 数字为0时只显示红点
// 或者直接
badge.setVisible(true);
TOP_END,但不同版本的MDC库,位置偏移量不一样。如果你发现Badge跑到图标外面去了,可以手动设置setBadgeGravity和setHorizontalOffset/setVerticalOffset来微调。另外,getOrCreateBadge这个方法在1.2.0版本之后才稳定,低版本可能会崩溃。
3. 与ViewPager联动:这才是实战
单独用BottomNavigationView没什么意思,真正干活的时候,都是和ViewPager2配合使用的。我见过很多新手直接手动切换Fragment,其实完全没必要——ViewPager2自带页面切换能力,我们只需要把导航选中状态和页面滑动状态同步起来就行。
来看一个完整的联动方案:
// 假设你已经有了ViewPager2和Adapter
ViewPager2 viewPager = findViewById(R.id.view_pager);
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
// 1. ViewPager页面切换时,更新底部导航选中状态
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
// 根据position选中对应的menu item
bottomNav.getMenu().getItem(position).setChecked(true);
}
});
// 2. 底部导航点击时,切换ViewPager页面
bottomNav.setOnItemSelectedListener(item -> {
int position = getPositionByItemId(item.getItemId());
if (position != -1) {
viewPager.setCurrentItem(position, false); // false表示不带动画
return true;
}
return false;
});
这里有个关键点——setCurrentItem的第二个参数。我建议设为false,也就是关闭滑动动画。为什么?因为底部导航点击是用户主动操作,不需要过渡动画,直接跳转反而更干脆。但如果是手指滑动ViewPager,动画是自然的,不需要干预。
4. 避坑指南:那些年我遇到的坑
实战中总会遇到各种奇怪的问题。我整理了几个高频坑,你遇到了可以直接翻出来看。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 底部导航被键盘顶起 | Activity的windowSoftInputMode设置不当 |
在AndroidManifest中设置android:windowSoftInputMode="adjustResize",或者用adjustPan。我个人习惯用adjustResize配合ConstraintLayout。 |
| Badge数字不更新 | 没有调用invalidateBadge()或重新设置 |
每次更新数字后,调用badge.invalidateSelf()强制刷新。 |
| ViewPager滑动和导航点击冲突 | 两者同时触发了页面切换 | 在onPageSelected中加一个标志位,避免重复触发。或者用setCurrentItem的smoothScroll参数控制。 |
| 菜单项超过4个 | BottomNavigationView默认最多显示5个,超过会变成NavigationView样式 |
如果必须超过5个,建议改用NavigationRailView或者自定义布局。我个人不建议超过5个tab,用户记不住。 |
5. 知识体系总览
下面这张图,是我自己整理的知识结构。你可以把它当作一个快速索引,用到哪个知识点,直接定位到对应部分。
6. 进阶技巧:自定义样式
如果你觉得默认的底部导航太死板,可以做一些定制。比如改变选中图标的大小、添加背景动画等。
一个常见的需求:选中时图标放大。实现方式是在setOnItemSelectedListener中动态修改图标大小:
bottomNavigation.setOnItemSelectedListener(item -> {
// 先重置所有图标大小
for (int i = 0; i < bottomNavigation.getMenu().size(); i++) {
MenuItem menuItem = bottomNavigation.getMenu().getItem(i);
Drawable icon = menuItem.getIcon();
if (icon != null) {
icon.setBounds(0, 0, 24, 24); // 默认大小
}
}
// 选中项放大
Drawable selectedIcon = item.getIcon();
if (selectedIcon != null) {
selectedIcon.setBounds(0, 0, 32, 32); // 放大
}
return true;
});
嗯,这个方法有点粗暴,但胜在简单。如果你追求更流畅的动画效果,可以考虑用Animator或者Transition来实现平滑缩放。
好了,关于BottomNavigationView的核心用法就这些。从基础配置到Badge,再到ViewPager联动,再到避坑指南,基本上覆盖了日常开发中90%的场景。剩下的10%,等你真正遇到问题时,再回来翻翻这篇文章,应该能找到答案。
公众号:蓝海资料掘金营,微信deep3321