BottomNavigationView从入门到强行改造,取消位移动画?和ViewPager绑定?添加Badge?

原创版权申明:本文章从本人 csdn 博客转到简书。
如有转载,请申明:转载自 IT天宇http://www.jianshu.com/p/56ae38b2433d

前言

BottomNavigationView 这个官方控件出了几个月了,也有一些介绍该控件的文章,但我发现大部分博文只是做了简单的用法介绍,并未解决一些需求,比如:取消位移动画、和ViewPager一起使用、加入Badge。所以我又写了这么一篇博客。

考虑到一些人可能没时间看到最后,我把改造的库地址放在最前面 BottomNavigationViewEx

基本用法

1. 添加依赖

compile 'com.android.support:design:25.1.0'

这里添加的是 25.1.0,因为 25.0.0 版本有一个小bug,就是设置点击监听事件的返回值不起作用。

2. 在 xml 中使用库

<android.support.design.widget.BottomNavigationView
    android:id="@+id/bnve"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="@color/colorPrimary"
    app:itemIconTint="@color/selector_item_color"
    app:itemTextColor="@color/selector_item_color"
    app:menu="@menu/menu_navigation_with_view_pager" />

background : 控件背景
app:itemBackground : 子菜单背景
app:itemIconTint : 图标颜色
app:itemTextColor : 文本颜色
app:menu : 菜单

这里我把背景设置成主色调 colorPrimary,图标和文本设置为一样的颜色 selector_item_color,具体内容如下:

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

也就是选中的时候是白色,默认为灰色。

最后是菜单 menu_navigation_with_view_pager ,和普通菜单一样。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_music"
        android:checked="true"
        android:icon="@drawable/ic_audiotrack_black_24dp"
        android:title="@string/music" />
    <item
        android:id="@+id/menu_backup"
        android:icon="@drawable/ic_backup_black_24dp"
        android:title="@string/backup" />
    <item
        android:id="@+id/menu_friends"
        android:icon="@drawable/ic_camera_black_24dp"
        android:title="@string/friends" />
</menu>

菜单的图片建议用矢量图片,也就是 svg 导入后的xml文件。

最后运行出来是这样的。

看到这里,我只能说,没毛病。

3. 方法

如果去看类的文档,就会发现,公开的方法中,常用的就只有 setOnNavigationItemSelectedListener

也就是设置一个点击的监听器。

// set listener to do something then item selected
bnve.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        Log.d(TAG, item.getItemId() + " item was selected-------------------");
        // you can return false to cancel select
        return true;
    }
});

回调方法有个返回值,如果返回 false,则你的点击会被取消,也就是不会切换到下一个菜单。
回调方法中给的参数是 MenuItem,你可以获取到被点击菜单的 id,也就是你可以这么做。

int id = 0;
switch (item.getItemId()) {
    case R.id.menu_music:
        id = 0;
        break;
    case R.id.menu_backup:
        id = 1;
        break;
    case R.id.menu_friends:
        id = 2;
        break;
}
vp.setCurrentItem(id, false);// 改变的 ViewPager 的当前页面

貌似依旧没毛病,官方的库用法简单实用。

官方库的需求问题

1. 和 ViewPager 一起使用

但仔细想一下,如果我想滑动 ViewPager 时,顺便改变控件的选中项(Material Design 反对这样设计,但需求确实存在)。

2. 取消位移动画

如果你的菜单数大于3个,则界面是这样的。


如果 PM 非要你改成没有动画的效果,如下图,这库是不是就很难用了?


3. 加入 Badge

对于底部导航栏,一个带数字的小红圈是很常见的需求,对于这种需求,又该怎么办?


动手改造

由于种种原因,官方的底部导航栏目前满足不了我的需求,所以我产生了改造库的想法。
大致有两种途径:

  1. 把整个控件代码复制一份,然后进行修改。
  2. 把类包裹一层,利用反射去修改。

这两种途径各有优缺点。

第一种直接修改的途径,优点是简单直接,性能高。缺点是需要把整个控件的代码都复制一份,每次官方对控制做出修改后,无法享受新特性。
第二种包裹的途径,优点是只需要针对一个类进行包裹,不容易影响到原来类的作用。缺点是反射性能不高。

在权衡一番之后,我选择了第二种方式。

分析源码

要改造库,首先得了解库的内部原理。

1. BottomNavigationView

进入 BottomNavigationView,发现最主要的成员是下面两个,由变量命名可以猜测出分别是作为视图和控制器。

private final BottomNavigationMenuView mMenuView;
private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();

然后看构造方法,是把 mMenuView 添加到Layout里了。所以,如果想要了解界面怎么显示的,还得分析 BottomNavigationMenuView

addView(mMenuView, params);

2. BottomNavigationMenuView

通过对成员变量的粗略查看,发现以下几个关键的成员。

private final OnClickListener mOnClickListener;// 点击监听器
private boolean mShiftingMode = true;// 控制导航条的位移模式
private BottomNavigationItemView[] mButtons;// 子菜单View

然后再看构造函数,设置了一个点击监听器,接收到的是 BottomNavigationItemView,处理的是点击子菜单的事件。

mOnClickListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
        final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
        final int itemPosition = itemView.getItemPosition();
        if (!mMenu.performItemAction(itemView.getItemData(), mPresenter, 0)) {
            activateNewButton(itemPosition);
        }
    }
};

然而这里并没有直接的对 mButtons 赋值,这个时候就应该去找 presenter,对MVP熟悉的就知道, presenter 负责把 M 和 V 联系起来。
在 presenter 的 updateMenuView 方法中调用了 mMenuView 中的 updateMenuView 去创建 mButtons。而 BottomNavigationView 中负责调用 presenter。

具体调用顺序如下:

BottomNavigationView()// BottomNavigationView
->
    inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));// BottomNavigationView 
    ->
        mPresenter.updateMenuView(true);// BottomNavigationView
        ->
            mMenuView.updateMenuView();// BottomNavigationPresenter
            ->
                buildMenuView();// BottomNavigationMenuView
                ->
                    mButtons = new BottomNavigationItemView[mMenu.size()];// BottomNavigationMenuView

所以,最后控制每个子菜单怎么显示的是 mButtons ,也就是 BottomNavigationItemView 。

3. BottomNavigationItemView

查看成员变量,发现负责显示的成员。

private boolean mShiftingMode;// 子菜单的位移模式
private ImageView mIcon;// 图片
private final TextView mSmallLabel;// 小文本
private final TextView mLargeLabel;// 大文本

分析到这里,基本算是了解主线了。
底部菜单是由一个一个 BottomNavigationItemView 组成,而 BottomNavigationItemView 是由 ImageViewTextView 组成的。

取消位移动画

分析完源码后,发现最容易做的是取消位移动画,因为在分析过程中,我发现了一个重要的 boolean 成员变量,从名字就可以看出是控制位移动画的。事实上,这猜测也是正确的,在代码里搜索 mShiftingMode 就会发现根据这个变量的真假,会有两套显示效果。这里就不展开了,毕竟不是专门分析源码的博文。

由于变量是私有的,且没有提供 set 方法,所以只能通过反射来做。

这个位移变量有两处,控制的内容是不一样的,我们先看 BottomNavigationMenuView 里面的。

1. BottomNavigationMenuView 中的 mShiftingMode

这里的 mShiftingMode 控制的是菜单之间的宽度,具体不太好说,对照上面的图片就容易理解的,选中的宽度大。
要想修改这个变量,必须先取得 mMenuView,然后在设置里面的 mShiftingMode。具体的代码如下。

/**
 * enable the shifting mode for navigation
 *
 * @param enable It will has a shift animation if true. Otherwise all items are the same width.
 */
public void enableShiftingMode(boolean enable) {
    /*
    1. get field in this class
    private final BottomNavigationMenuView mMenuView;

    2. change field mShiftingMode value in mMenuView
    private boolean mShiftingMode = true;
     */
    // 1. get mMenuView
    BottomNavigationMenuView mMenuView = getBottomNavigationMenuView();
    // 2. change field mShiftingMode value in mMenuView
    setField(mMenuView.getClass(), mMenuView, "mShiftingMode", enable);

    mMenuView.updateMenuView();
}

这里没有把反射细节代码写出来,因为反射很简单,只是步骤繁琐,所以节省篇幅,就略过,有兴趣可以查看我写的库的代码。

2. BottomNavigationItemView 中的 mShiftingMode

这个位移模式是只文字的显示,如果开启,则选择项显示图标和文字,其他的只显示图片。
修改方法和上面类似。

/**
 * enable the shifting mode for each item
 *
 * @param enable It will has a shift animation for item if true. Otherwise the item text always be shown.
 */
public void enableItemShiftingMode(boolean enable) {
    /*
    1. get field in this class
    private final BottomNavigationMenuView mMenuView;

    2. get field in this mMenuView
    private BottomNavigationItemView[] mButtons;

    3. change field mShiftingMode value in mButtons
    private boolean mShiftingMode = true;
     */
    // 1. get mMenuView
    BottomNavigationMenuView mMenuView = getBottomNavigationMenuView();
    // 2. get mButtons
    BottomNavigationItemView[] mButtons = getBottomNavigationItemViews();
    // 3. change field mShiftingMode value in mButtons
    for (BottomNavigationItemView button : mButtons) {
        setField(button.getClass(), button, "mShiftingMode", enable);
    }
    mMenuView.updateMenuView();
}

设置当前选中项

还记得在 BottomNavigationMenuView 看到的 mOnClickListener 吗?
那个就是关键,只要能模拟发出一个 click 事件,就能设置当前选中项。

onClick 方法需要传递一个 View,而且是 BottomNavigationItemView
为了调用这一方法,需要先获取到对应位置的 BottomNavigationItemView。而这个 View 似曾相识。
没错,就是 mButtons ,只要取得了 mButtons,然后获取数组对应位置的值,就是这个参数了。
具体代码如下:

    /**
     * set the current checked item
     *
     * @param item start from 0.
     */
    public void setCurrentItem(int item) {
        // check bounds
        if (item < 0 || item >= getMaxItemCount()) {
            throw new ArrayIndexOutOfBoundsException("item is out of bounds, we expected 0 - "
                    + (getMaxItemCount() - 1) + ". Actually " + item);
        }

        /*
        1. get field in this class
        private final BottomNavigationMenuView mMenuView;

        2. get field in mMenuView
        private BottomNavigationItemView[] mButtons;
        private final OnClickListener mOnClickListener;

        3. call mOnClickListener.onClick();
         */
        // 1. get mMenuView
        BottomNavigationMenuView mMenuView = getBottomNavigationMenuView();
        // 2. get mButtons
        BottomNavigationItemView[] mButtons = getBottomNavigationItemViews();
        // get mOnClickListener
        View.OnClickListener mOnClickListener = getField(mMenuView.getClass(), mMenuView, "mOnClickListener", View.OnClickListener.class);

//        System.out.println("mMenuView:" + mMenuView + " mButtons:" + mButtons + " mOnClickListener" + mOnClickListener);
        // 3. call mOnClickListener.onClick();
        mOnClickListener.onClick(mButtons[item]);

    }

加入 Badge

Bagde 就是字面意思,一个标记。一般都是一个小红圈,里面有数字。
给控件加上 Bagde 的思路大致有以下几种:

  1. 给控件加个红点 ImageView
  2. 给控件图片的 Drawable 外面套一个带红点的 Drawable,然后替换 Drawable。
  3. 在顶级容器上加入小红点,调整位置,伪装成和控件一体。

事实上这几种方法对于底部导航栏来说都行得通。
但实现起来难度不一样。
我为了省事,直接用了第三方库 BadgeView

本想采用第一种方法,但发现,加在图片或 BottomNavigationItemView 上都会导致排版错乱。
于是尝试第三种方案,发现行得通。

具体代码如下:

private void initView() {
    // disable all animations
    bind.bnve.enableAnimation(false);
    bind.bnve.enableShiftingMode(false);
    bind.bnve.enableItemShiftingMode(false);


    // add a BadgeView at second icon
    bind.bnve.post(new Runnable() {
        @Override
        public void run() {
            badgeView1 = addBadgeViewAt(1, "1", BadgeView.SHAPE_OVAL);
            badgeView3 = addBadgeViewAt(3, "99", BadgeView.SHAPE_OVAL);

            // hide the red circle when click
            bind.bnve.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                    int position = bind.bnve.getMenuItemPosition(item);
                    switch (position) {
                        case 1:
                            toggleBadgeView(badgeView1);
                            break;
                        case 3:
                            toggleBadgeView(badgeView3);
                            break;
                    }
                    return true;
                }
            });
        }
    });

}

/**
 * show or hide badgeView
 * @param badgeView
 */
private void toggleBadgeView(BadgeView badgeView) {
    badgeView.setVisibility(badgeView.getVisibility() == View.VISIBLE ? View.INVISIBLE : View.VISIBLE);
}

/**
 * add a BadgeView on icon at position
 * @param position add to which icon
 * @param text the text show on badge
 * @param shape the badge view shape
 * @return
 */
private BadgeView addBadgeViewAt(int position, String text, int shape) {
    // get position
    ImageView icon = bind.bnve.getIconAt(position);
    int[] pos = new int[2];
    icon.getLocationInWindow(pos);
    // action bar height
    ActionBar actionBar = getSupportActionBar();
    int actionBarHeight = 0;
    if (null != actionBar) {
        actionBarHeight = actionBar.getHeight();
    }
    int x = (int) (pos[0] + icon.getMeasuredWidth() * 0.7f);
    int y = (int) (pos[1] - actionBarHeight - icon.getMeasuredHeight() * 1.25f);
    // calculate width
    int width = 16 + 4 * (text.length() - 1);
    int height = 16;

    BadgeView badgeView = BadgeFactory.create(this)
            .setTextColor(Color.WHITE)
            .setWidthAndHeight(width, height)
            .setBadgeBackground(Color.RED)
            .setTextSize(10)
            .setBadgeGravity(Gravity.LEFT | Gravity.TOP)
            .setBadgeCount(text)
            .setShape(shape)
//                .setMargin(0, 0, 0, 0)
            .bind(this.bind.rlRoot);
    badgeView.setX(x);
    badgeView.setY(y);
    return badgeView;
}

把红点加载根布局上,然后获取到目标图片的位置,计算出间距就行了。

代码放在了 Github ,地址在最前面,若有兴趣,记得 star 收藏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容