41、Material Design之BottomNavigationView:底部导航、Badge、与ViewPager联动

底部导航栏,几乎是每个App的标配。你想想看,微信、支付宝、淘宝,哪个不是底部四个tab?用户早就习惯了这种操作方式。今天我们就来聊聊Android里最正统的底部导航实现——BottomNavigationView

说实话,我刚入行那会儿,底部导航还是用RadioGroupFragment硬拼出来的。后来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" />

这里有个小细节——itemIconTintitemTextColor。我建议你用一个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);
我曾经踩过的坑:Badge的显示位置默认是TOP_END,但不同版本的MDC库,位置偏移量不一样。如果你发现Badge跑到图标外面去了,可以手动设置setBadgeGravitysetHorizontalOffset/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,动画是自然的,不需要干预。

核心逻辑:底部导航和ViewPager是双向绑定的。导航点击驱动页面切换,页面滑动驱动导航选中。两者互不冲突,各司其职。

4. 避坑指南:那些年我遇到的坑

实战中总会遇到各种奇怪的问题。我整理了几个高频坑,你遇到了可以直接翻出来看。

问题 原因 解决方案
底部导航被键盘顶起 Activity的windowSoftInputMode设置不当 在AndroidManifest中设置android:windowSoftInputMode="adjustResize",或者用adjustPan。我个人习惯用adjustResize配合ConstraintLayout
Badge数字不更新 没有调用invalidateBadge()或重新设置 每次更新数字后,调用badge.invalidateSelf()强制刷新。
ViewPager滑动和导航点击冲突 两者同时触发了页面切换 onPageSelected中加一个标志位,避免重复触发。或者用setCurrentItemsmoothScroll参数控制。
菜单项超过4个 BottomNavigationView默认最多显示5个,超过会变成NavigationView样式 如果必须超过5个,建议改用NavigationRailView或者自定义布局。我个人不建议超过5个tab,用户记不住。

5. 知识体系总览

下面这张图,是我自己整理的知识结构。你可以把它当作一个快速索引,用到哪个知识点,直接定位到对应部分。

BottomNavigationView 基础配置 菜单资源文件 布局文件配置 事件监听绑定 Badge 小红点 显示数字/红点 位置偏移调整 可见性控制 ViewPager2 双向联动

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来实现平滑缩放。

我的建议:不要过度设计底部导航。用户对底部导航的预期就是「稳定、快速、可预测」。花里胡哨的动画反而会让用户觉得卡顿。我见过一个App,底部导航切换时有个旋转动画,结果用户反馈「App好卡」——其实不是卡,是动画太长了。

好了,关于BottomNavigationView的核心用法就这些。从基础配置到Badge,再到ViewPager联动,再到避坑指南,基本上覆盖了日常开发中90%的场景。剩下的10%,等你真正遇到问题时,再回来翻翻这篇文章,应该能找到答案。


公众号:蓝海资料掘金营,微信deep3321