Android Material Design 系列之 BottomNavigationView + ViewPager + BadgeView 开发详解

5,893 阅读9分钟

前言

BottomNavigationView 是 Material Design 提供的一个标准底部导航栏的实现,可以轻松的实现导航栏菜单之间的切换与浏览。底部导航使用户更方便的查看和切换最高层级的导航界面,适用于有三到五个 Tab 的情况

APP 底部导航栏目中,有新消息提示用户,并在导航栏底部显示具体消息数,这种效果主流 APP 都有应用。本文就介绍 BottomNavigationView + ViewPager + Fragment + BadgeView 可以达到微信消息角标效果和 QQ 消息拖拽效果。

一、BottomNavigationView 相关属性

方法 介绍
setSelectedItemId(int itemId) 设置选择的菜单项 ID
setElevation(float elevation) 设置此视图的基本高程(以像素为单位)
setItemBackground(Drawable background) 将菜单项的背景设置为给定的可绘制对象
setItemBackgroundResource(int resId) 将菜单项的背景设置为给定资源
setItemIconSize(int iconSize) 设置大小以提供菜单项图标
setItemIconTintList(ColorStateList tint) 设置应用于菜单项图标的色彩
setItemRippleColor(ColorStateList itemRippleColor) 将菜单项的背景设置为具有给定颜色的波纹
setItemTextAppearanceActive(int textAppearanceRes) 设置用于菜单项标签的文本样式
setItemTextAppearanceInactive(int textAppearanceRes) 设置用于非活动菜单项标签的文本样式
setItemTextColor(ColorStateList textColor) 设置颜色以用于菜单项文本的不同状态(正常,选中,聚焦等)
setLabelVisibilityMode(int labelVisibilityMode) 设置导航项目的标签可见性模式
setItemHorizontalTranslationEnabled(boolean itemHorizontalTranslationEnabled) 设置当合并的项目宽度填满屏幕时,菜单项是否在选择时水平平移
setOnNavigationItemReselectedListener(BottomNavigationView.OnNavigationItemReselectedListener listener) 设置一个侦听器,当重新选择当前选择的底部导航项时将通知该侦听器
getMenu() 返回 Menu 与此底部导航栏关联的实例
getMaxItemCount() 返回最大 Menu 数量
  • setSelectedItemId(int itemId) 设置选择的菜单项 ID。

  • setElevation(float elevation) 设置此视图的基本高程(以像素为单位)。

  • setItemBackground(Drawable background) 将菜单项的背景设置为给定的可绘制对象。

  • setItemBackgroundResource(int resId) 将菜单项的背景设置为给定资源。

  • setItemIconSize(int iconSize) 设置大小以提供菜单项图标。

  • setItemIconTintList(ColorStateList tint) 设置应用于菜单项图标的色彩。

  • setItemRippleColor(ColorStateList itemRippleColor) 将菜单项的背景设置为具有给定颜色的波纹。

  • setItemTextAppearanceActive(int textAppearanceRes) 设置用于菜单项标签的文本样式。

  • setItemTextAppearanceInactive(int textAppearanceRes) 设置用于非活动菜单项标签的文本样式。

  • setItemTextColor(ColorStateList textColor) 设置颜色以用于菜单项文本的不同状态(正常,选中,聚焦等)。

  • setLabelVisibilityMode(int labelVisibilityMode) 设置导航项目的标签可见性模式。

  • setItemHorizontalTranslationEnabled(boolean itemHorizontalTranslationEnabled) 设置当合并的项目宽度填满屏幕时,菜单项是否在选择时水平平移。

  • setOnNavigationItemReselectedListener(BottomNavigationView.OnNavigationItemReselectedListener listener) 设置一个侦听器,当重新选择当前选择的底部导航项时将通知该侦听器。

  • setOnNavigationItemSelectedListener(BottomNavigationView.OnNavigationItemSelectedListener listener) 设置一个侦听器,当选择底部导航项时将通知该侦听器。

  • getMenu() 返回 Menu 与此底部导航栏关联的实例。

  • getMaxItemCount() 返回最大 Menu 数量。

二、BottomNavigationView 基础使用

1、依赖引入

implementation 'com.google.android.material:material:1.0.0-alpha1'

2、XML 布局文件

在 xml 布局文件中直接引入控件

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottomNavigationView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:menu="@menu/activity_bottom_nav" />

3、menu 创建

BottomNavigationView 需要添加一个 menu 布局,之前在 DarwerLayout+NavigationView 文章中讲过 menu 创建注意事项,底部导航栏使用至少需要 android:id、android:icon、android:title 三个属性,此处就不做具体讲解,直接附上代码:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_item_home"
        android:icon="@drawable/ic_vector_bottom_home"
        android:title="主页" />

    <item
        android:id="@+id/menu_item_project"
        android:icon="@drawable/ic_vector_bottom_book"
        android:title="项目" />

    <item
        android:id="@+id/menu_item_movie"
        android:icon="@drawable/ic_vector_bottom_movie"
        android:title="电影" />
</menu>

4、三个以上 Tab 文本与动画效果

经过以上三步,基本上就可以看到 BottomNavigationView 的展示效果,但是发现 3 个以上菜单时会出现文本显示异常现象,动画效果也变了。要想解决这个问题,在最新版的 API 中,已经为大家提供了解决方案,只需要一行代码就可以完美解决。

bottomNavigationView.setLabelVisibilityMode(LABEL_VISIBILITY_LABELED);
//或者在xml文件中使用属性
app:labelVisibilityMode="labeled"

因为本文是根据 androidx 讲解,如果需要旧版本兼容,使用以下方法:

public static void disableShiftMode(BottomNavigationView view) {
    BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
    try {
        Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
        shiftingMode.setAccessible(true);
        shiftingMode.setBoolean(menuView, false);
        shiftingMode.setAccessible(false);
        for (int i = 0; i < menuView.getChildCount(); i++) {
            BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
            //去除动画
            item.setShiftingMode(false); //api 28之前
            item.setChecked(item.getItemData().isChecked());
        }
    } catch (NoSuchFieldException e) {
        LogUtils.e( "Unable to get shift mode field");
    } catch (IllegalAccessException e) {
        LogUtils.e( "Unable to change value of shift mode");
    }
}

5、设置选中菜单颜色

1、在 color 文件夹下新建 selector_nav_text_item.xml 文件

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/white" android:state_checked="true"  />
    <item android:color="@color/white" android:state_pressed="true" />
    <item android:color="@color/md_grey_700"/>
</selector>

2、设置颜色属性

app:itemIconTint="@color/selector_nav_text_item"
app:itemTextColor="@color/selector_nav_text_item"

6、设置 Item 选中

// index表示tab索引
bottomNavigationView.getMenu().getItem(index).setChecked(true);

7、设置选中监听器

BottomNavigationView 设置监听器,可以根据 Item 的 id 判断点击选中项,这里可以做很多事情,比如 ViewPager 和 Fragment 切换,设置 Item 样式等等。

bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
        switch (menuItem.getItemId()) {
            case R.id.menu_item_home:
                Toast.makeText(BottomNavigationViewActivity.this, "主页", Toast.LENGTH_SHORT).show();
                bottomNavigationView.setBackgroundColor(getResources().getColor(R.color.light_yellow));
                break;
            case R.id.menu_item_project:
                Toast.makeText(BottomNavigationViewActivity.this, "项目", Toast.LENGTH_SHORT).show();
                bottomNavigationView.setBackgroundColor(getResources().getColor(R.color.brown));
                break;
            case R.id.menu_item_movie:
                Toast.makeText(BottomNavigationViewActivity.this, "电影", Toast.LENGTH_SHORT).show();
                bottomNavigationView.setBackgroundColor(getResources().getColor(R.color.txt_link_blue));
                break;
            case R.id.menu_item_book:
                Toast.makeText(BottomNavigationViewActivity.this, "干货", Toast.LENGTH_SHORT).show();
                bottomNavigationView.setBackgroundColor(getResources().getColor(R.color.md_lime_700));
                break;
            case R.id.menu_item_personal:
                Toast.makeText(BottomNavigationViewActivity.this, "个人", Toast.LENGTH_SHORT).show();
                bottomNavigationView.setBackgroundColor(getResources().getColor(R.color.md_yellow_500));
                break;
        }
        return true;
    }
});

三、BottomNavigationView + ViewPager + Fragment 详解

BottomNavigationView 往往配合 ViewPager 和 Fragment 一起使用,在 APP 开发中,这是最基本的框架结构。之前文章中讲解过 TabLayout+ViewPager+Fragmemt 的使用,所以本文不再对 ViewPager+Fragment 重复讲解。感兴趣的可以查看Android Material Design 系列之 TabLayout + ViewPager + Fragment 使用详解

1、ViewPager 滑动同步 BottomNavigationView

监听 ViewPager 滑动事件,在onPageSelected(int position)方法中返回当前页面 position,然后通过上面讲到的 bottomNavigationView.getMenu().getItem(position).setChecked(true);方法设置当前选中项。

viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
        bottomNavigationView.getMenu().getItem(position).setChecked(true);
        // TODO:更新ToolBar标题栏
        setToolBarTitle(position);
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }
});

2、BottomNavigationView 切换同步 ViewPager

同理,监听 BottomNavigationView 选中接口,在回调方法onNavigationItemSelected(MenuItem menuItem)中,根据 ItemID 更新 ViewPager 选项卡,通过调用viewPager.setCurrentItem()方法实现;

bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
        switch (menuItem.getItemId()) {
            case R.id.menu_item_home:
                Toast.makeText(BottomNavigationViewActivity.this, "主页", Toast.LENGTH_SHORT).show();
                viewPager.setCurrentItem(0, false);
                break;
            case R.id.menu_item_project:
                Toast.makeText(BottomNavigationViewActivity.this, "项目", Toast.LENGTH_SHORT).show();
                viewPager.setCurrentItem(1, false);
                break;
            case R.id.menu_item_movie:
                Toast.makeText(BottomNavigationViewActivity.this, "电影", Toast.LENGTH_SHORT).show();
                viewPager.setCurrentItem(2, false);
                break;
            case R.id.menu_item_book:
                Toast.makeText(BottomNavigationViewActivity.this, "干货", Toast.LENGTH_SHORT).show();
                viewPager.setCurrentItem(3, false);
                break;
            case R.id.menu_item_personal:
                Toast.makeText(BottomNavigationViewActivity.this, "个人", Toast.LENGTH_SHORT).show();
                viewPager.setCurrentItem(4, false);
                break;
        }
        return true;
    }
});

3、设置 Toolbar 标题栏

通常情况下,APP 每个界面的标题栏均不相同,为了方便将所有 ToolBar 放在 MainActivity 中管理,根据 ViewPager 监听回调方法中动态设置 Toolbar 的标题栏,调用代码在ViewPager 滑动同步 BottomNavigationView目录中已经贴出,这里附加更新 ToolBar 方法。

/**
 * 更新Toolbar标题
 * @param currentTitleIndex
 */
private void setToolBarTitle(int currentTitleIndex) {
    toolbar.setTitle(bottomNavigationView.getMenu().getItem(currentTitleIndex).getTitle());
}

四、BadgeView 介绍

1、定义

一个可以自由定制外观、支持拖拽消除的 MaterialDesign 风格 Android BadgeView

2、特性

  • 随意定制外观,包括 Badge 位置、底色、边框、阴影、文字颜色(支持透明色)、大小、内外边距等

  • Badge 数字小于 0 时显示 dot,等于 0 时隐藏整个 Badge,在普通模式下超过 99 时显示 99+,精确模式下显示具体值

  • 支持设置文本内容

  • 支持设置图片背景

  • 支持类似 QQ 的拖拽消除效果(默认关闭)

  • 支持以动画的方式隐藏 Badge

五、BadgeView 方法说明

方法 介绍
getBadgeNumber 设置 Badge 数字
setBadgeText 设置 Badge 文本
setBadgeTextSize 设置 Badge 文本字体大小
setBadgeTextColor 设置 Badge 文本字体颜色
setBadgeGravity 设置 Badge 在 TargetView 的位置
setExactMode 设置是否显示精确数字模式
setGravityOffset 设置外边距
setBadgePadding 设置内边距
setBadgeBackgroundColor 设置背景颜色
setBadgeBackground 设置背景图片
setShowShadow 设置是否显示阴影效果
stroke 描边
hide 隐藏 Badge
setOnDragStateChangedListener 开启拖拽消除并监听

六、BottomNavigationView + BadgeView 使用详解

1、依赖包引入

implementation 'q.rorbin:badgeview:1.1.3'

2、获取 BottomNavigationMenuView

Badge 需要绑定一个 View,所有方法和操作都是以这个 View 为中心的。因为本文讲的是 BottomNavigationView,所以就以 BottomNavigationView 的 MenuItemView 为中心。

查看源码可以看到 BottomNavigationMenuView 是 BottomNavigationView 内部添加的一个子 View,也就是说他是导航栏中添加的所有 Menu 的一个父 View,那么获取 BottomNavigationMenuView 以及子 Menu 就很简单了。

BottomNavigationMenuView itemView = (BottomNavigationMenuView) bottomNav.getChildAt(0);
// 获取导航栏Tab数量
int childCount = itemView.getChildCount();

3、BadgeView 初始化

Badge 是一个接口,创建实现类 QBadgeView 对象,然后设置相关属性。

QBadgeView qBadgeView=new QBadgeView(context);

4、BadgeView 基础用法

BadgeView 设置绑定 View、数字、位置

new QBadgeView(getActivity()).bindTarget(itemView.getChildAt(0))
        .setBadgeNumber(6)
        .setBadgeGravity(Gravity.TOP | Gravity.END);

5、修改文本颜色

new QBadgeView(getActivity()).bindTarget(itemView.getChildAt(1))
        .setBadgeNumber(27)
        .setBadgeGravity(Gravity.TOP | Gravity.END)
        .setBadgeTextColor(Color.YELLOW)
        .setGravityOffset(10, 0, true);

6、设置是否为精确模式数

setExactMode(boolean isExact)方法设置为 false,当消息数>99,则显示 99+,若设置为 true,当消息数>99,则显示具体的消息数。

// 显示 99+
new QBadgeView(getActivity()).bindTarget(itemView.getChildAt(2))
        .setBadgeNumber(999)
        .setBadgeGravity(Gravity.TOP | Gravity.END)
        .setExactMode(false);
// 显示 1000
new QBadgeView(getActivity()).bindTarget(itemView.getChildAt(3))
        .setBadgeNumber(1000)
        .setBadgeGravity(Gravity.TOP | Gravity.END)
        .setExactMode(true);

7、BadgeView 阴影效果

setShowShadow(boolean showShadow)方法设置为 true 时表示有阴影效果,为 false 时取消阴影效果。

new QBadgeView(getActivity()).bindTarget(itemView.getChildAt(4))
        .setBadgeNumber(9)
        .setBadgeGravity(Gravity.TOP | Gravity.END)
        .setBadgeBackgroundColor(Color.BLUE)
        .setShowShadow(false);

8、BadgeView 拖拽效果

BadgeView 添加setOnDragStateChangedListener监听,即可实现仿 QQ 拖拽效果,本文在 BottomNavigationView 导航栏 Item 上使用,也可以在 RecyclerView 的 Item 上实现,使用极其简单。

回调函数onDragStateChanged其中 dragState 状态有五种:

  • int STATE_START = 1;
  • int STATE_DRAGGING = 2;
  • int STATE_DRAGGING_OUT_OF_RANGE = 3;
  • int STATE_CANCELED = 4;
  • int STATE_SUCCEED = 5;
new QBadgeView(getActivity()).bindTarget(itemView.getChildAt(4))
        .setBadgeNumber(9)
        .setBadgeGravity(Gravity.TOP | Gravity.END)
        .setOnDragStateChangedListener(new Badge.OnDragStateChangedListener() {
            @Override
            public void onDragStateChanged(int dragState, Badge badge, View targetView) {

            }
        });

以上只是挑选出来几个比较常用的属性做详细讲解,如果对其他属性感兴趣的可以自己写个案例尝试一下。

七、BadgeView 使用注意事项

  • 请不要在 xml 中创建 Badge
  • Badge 和 TargetView 绑定是采用替换 TargetView 的 Parent 方式实现的,同时将 Parent 的 Id 和 TargetView 的 Id 设置成一样来保证不会在 RelativeLayout 中出现位置错乱问题,所以在 bindTarget 后再次使用 findViewById(TargetViewId)得到的会是 Parent 而不是 TargetView,此时建议使用 Badge.getTargetView 方法来获取 TargetView。

源码下载 源码包含 Material Design 系列控件集合,定时更新,敬请期待!

总结

最开始我们实现 APP 首页框架的时候,大多数人应该都沉浸在 Tablayout + viewpager + Fragment 的世界,我相信还有部分人在使用 RadioGroup 和 RadioButton 实现。既然 Google 已经为开发者提供了 BottomNavigationView 控件,还在使用第三方控件以及原始模式的朋友们,可以放心的迁移过来了,Google 出品,必为精品,配合 BadgeView 完美实现底部导航栏。

非常感谢您阅读本篇文章!
您的点赞,您的点评是对我创作最大的动力!

我的微信:Jaynm888

程序员面试交流群:764040616

诚邀 Android 程序员加入微信交流群,公众号回复微信群或者加我微信邀请入群。