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 中使用库


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

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



    
    
    

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

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



    
    
    

菜单的图片建议用矢量图片,也就是 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 收藏。

你可能感兴趣的:(BottomNavigationView从入门到强行改造,取消位移动画?和ViewPager绑定?添加Badge?)