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

如有转载,请申明: 
转载至 http://blog.csdn.net/qq_35064774/article/details/54177702 

前言

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

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

基本用法

1. 添加依赖

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

这里添加的是 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" />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

最后是菜单 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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

菜单的图片建议用矢量图片,也就是 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;
    }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

回调方法有个返回值,如果返回 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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

官方库的需求问题

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();
  • 1
  • 2

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

addView(mMenuView, params);
  • 1

2. BottomNavigationMenuView

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

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

然后再看构造函数,设置了一个点击监听器,接收到的是 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);
        }
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

然而这里并没有直接的对 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

3. BottomNavigationItemView

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

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

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

取消位移动画

分析完源码后,发现最容易做的是取消位移动画,因为在分析过程中,我发现了一个重要的 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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

设置当前选中项

还记得在 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]);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

加入 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

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

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

猜你喜欢

转载自blog.csdn.net/huanglei201502/article/details/79402372