底部导航栏的实现方式多种多样,可以使用LineatLayout或者RadioGroup自定义控件,也可以直接使用第三方提供的如BottomNavigationBar、BottomBarLayout这些功能更多的控件。而如果我们只是想实现一个简单的只用来切换页面的底部导航栏,使用自定义控件的方法有一堆设置切换图标、selector之类的步骤太过繁琐,使用第三方的控件又有一种杀鸡用牛刀的感觉,因此我们可以使用官方提供的BottomNavigationView控件。

简单设置后的效果如图

android导航栏关闭 android底部导航栏切换_BottomNavigationView


1、BottomNavigationView使用前需要导入design包

implementation 'com.android.support:design:26.1.0'

这个控件的使用非常简单,根据源码的描述:

The bar contents can be populated by specifying a menu resource file. Each menu item title, icon
* and enabled state will be used for displaying bottom navigation bar items. Menu items can also be
* used for programmatically selecting which destination is currently active. It can be done using
* {@code MenuItem#setChecked(true)}

BottomNavigationView需要一个menu文件来设置导航栏每一项的标题和图标,然后在控件中使用app:menu="@menu/xxx"绑定这个menu文件,如下所示

* layout resource file:
* <android.support.design.widget.BottomNavigationView
*     xmlns:android="http://schemas.android.com/apk/res/android"
*     xmlns:app="http://schemas.android.com/apk/res-auto"
*     android:id="@+id/navigation"
*     android:layout_width="match_parent"
*     android:layout_height="56dp"
*     android:layout_gravity="start"
*     app:menu="@menu/my_navigation_items" />
*
* res/menu/my_navigation_items.xml:
* <menu xmlns:android="http://schemas.android.com/apk/res/android">
*     <item android:id="@+id/action_search"
*          android:title="@string/menu_search"
*          android:icon="@drawable/ic_search" />
*     <item android:id="@+id/action_settings"
*          android:title="@string/menu_settings"
*          android:icon="@drawable/ic_add" />
*     <item android:id="@+id/action_navigation"
*          android:title="@string/menu_navigation"
*          android:icon="@drawable/ic_action_navigation_menu" />
* </menu>


2、xml文件

首先新建一个在res下新建一个menu目录并新建一个menu文件,在文件中设置导航栏每项的title和icon

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/bottom_menu_home"
        android:icon="@mipmap/homepage"
        android:title="首页" />
    <item
        android:id="@+id/bottom_menu_found"
        android:icon="@mipmap/find"
        android:title="更多" />
    <item
        android:id="@+id/bottom_menu_message"
        android:icon="@mipmap/message"
        android:title="消息" />
    <item
        android:id="@+id/bottom_menu_user"
        android:icon="@mipmap/avatar"
        android:title="我" />
</menu>

然后在layout文件中使用BottomNavigationView,ViewPager是内容主体容器,最下面的View是导航栏顶部的阴影效果

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/bottom_navigation">

    </android.support.v4.view.ViewPager>

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:itemIconTint="@drawable/bottom_navigation_selector"
        app:itemTextColor="@drawable/bottom_navigation_selector"
        app:menu="@menu/bottom_menu" />

    <View
        android:layout_width="match_parent"
        android:layout_height="5dp"
        android:layout_above="@id/bottom_navigation"
        android:background="@drawable/bottom_shadow" />

</RelativeLayout>

itemIconTint是为图标着色,itemTextColor是标题颜色,这里可以使用了一个selector,让选中的item和未选中的item展现不同颜色

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


3、在Activity中的代码实现

变量声明(menuItem负责展现item选中/未选中的样式变化)

private BottomNavigationView bottomNavigationView;

    private MenuItem menuItem;

    private ViewPager viewPager;

设置导航栏的选中事件:setOnNavigationItemSelectedlistener(),这里的选中事件是让viewPager移动到指定页面

bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                switch (item.getItemId()){
                    case R.id.bottom_menu_home:
                        viewPager.setCurrentItem(0);
                        break;
                    case R.id.bottom_menu_found:
                        viewPager.setCurrentItem(1);
                        break;
                    case R.id.bottom_menu_message:
                        viewPager.setCurrentItem(2);
                        break;
                    case R.id.bottom_menu_user:
                        viewPager.setCurrentItem(3);
                        break;
                }
                return false;
            }
        });

viewPager的页面切换事件

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

            }

            @Override
            public void onPageSelected(int position) {
                if (menuItem != null) {
                    //如果有已选中的item,则取消它的的选中状态
                    menuItem.setChecked(false);
                } else {
                    //如果没有,则取消默认的选中状态(第一个item)
                    bottomNavigationView.getMenu().getItem(0).setChecked(false);
                }
                //让与当前Pager相应的item变为选中状态
                menuItem = bottomNavigationView.getMenu().getItem(position);
                menuItem.setChecked(true);
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });





4、取消移位效果

到此BotttomNavigationView已经可以正常使用了,但是运行起来我们会发现,底部导航栏显示的样式并非是我们想要的风格

android导航栏关闭 android底部导航栏切换_android_02

这是因为当item数量大于3的时候,选中item时默认有一个移位效果,选中的item显示完整的icon和title以及占据更多的宽度,显然这种效果不是我们想要的,查看BottomNavigationView的源码,先看看这种移位效果是通过什么来控制的。


BottomNavigationView

观察BottomNavigationView源码中的变量声明,可以发现导航栏的tabItem是通过一个BottomNavigatiMenuView来展示的

private final MenuBuilder mMenu;
private final BottomNavigationMenuView mMenuView;
private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();
private MenuInflater mMenuInflater;


BottomNavigationMenuView

查看BottomNavigaMenuView的源码,观察变量声明,值得注意的是一个名为mShiftingMode的boolean型变量和一个BottomNavigationItemView类型的数组

private boolean mShiftingMode = true;

private BottomNavigationItemView[] mButtons;

从字面意思上不难理解,mShiftingMode应该就是移位效果的开关了,而BottomNavigationItemView数组就是MenuView里面的每个tabItem,先看MenuView中的mShiftingMode有什么作用

if (mShiftingMode) {
    final int inactiveCount = count - 1;
    final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
    final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
    final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
    final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
    int extra = width - activeWidth - inactiveWidth * inactiveCount;
    for (int i = 0; i < count; i++) {
        mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
        if (extra > 0) {
            mTempChildWidths[i]++;
            extra--;
        }
    }
} else {
    final int maxAvailable = width / (count == 0 ? 1 : count);
    final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
    int extra = width - childWidth * count;
    for (int i = 0; i < count; i++) {
        mTempChildWidths[i] = childWidth;
        if (extra > 0) {
            mTempChildWidths[i]++;
            extra--;
        }
    }
}

通过源码可以看到,当移位效果开启的时候,选中的item宽度(activeWidth)和未选中的item宽度(inactiveWidth)明显是不同的,那么MenuView中的mShiftingMode应该就是用于控制tabItem的宽度了。


BottomNavigationItemView

接下来再查看BottomNavigationItemView的源码,观察变量,发现也有一个mShiftingMode变量

private boolean mShiftingMode;

同样继续看它的作用

if (mShiftingMode) {
    if (checked) {
        LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
        iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
        iconParams.topMargin = mDefaultMargin;
        mIcon.setLayoutParams(iconParams);
        mLargeLabel.setVisibility(VISIBLE);
        mLargeLabel.setScaleX(1f);
        mLargeLabel.setScaleY(1f);
    } else {
        LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
        iconParams.gravity = Gravity.CENTER;
        iconParams.topMargin = mDefaultMargin;
        mIcon.setLayoutParams(iconParams);
        mLargeLabel.setVisibility(INVISIBLE);
        mLargeLabel.setScaleX(0.5f);
        mLargeLabel.setScaleY(0.5f);
    }
    mSmallLabel.setVisibility(INVISIBLE);
} else {
    if (checked) {
        LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
        iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
        iconParams.topMargin = mDefaultMargin + mShiftAmount;
        mIcon.setLayoutParams(iconParams);
        mLargeLabel.setVisibility(VISIBLE);
        mSmallLabel.setVisibility(INVISIBLE);

        mLargeLabel.setScaleX(1f);
        mLargeLabel.setScaleY(1f);
        mSmallLabel.setScaleX(mScaleUpFactor);
        mSmallLabel.setScaleY(mScaleUpFactor);
    } else {
        LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
        iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
        iconParams.topMargin = mDefaultMargin;
        mIcon.setLayoutParams(iconParams);
        mLargeLabel.setVisibility(INVISIBLE);
        mSmallLabel.setVisibility(VISIBLE);

        mLargeLabel.setScaleX(mScaleDownFactor);
        mLargeLabel.setScaleY(mScaleDownFactor);
        mSmallLabel.setScaleX(1f);
        mSmallLabel.setScaleY(1f);
    }
}

可以看出ItemView中的mShiftingMode是用于控制每个item中的内容的位置以及显示的,也就是控制标题和图标的显示以及位置大小


回到Activity取消移位效果

现在已经知道BottomNavigationView的移位效果的开关了,但是从MenuView的源码中,我们并没有发现能够从外部修改这个开关的方法,因此,要改变mShiftingMode的值,只能通过反射机制来实现了,在Activity中声明一个关闭移位效果的方法:通过反射制将获取到MenuView中的mShiftingMode,将其设为false,再遍历menuView的子项item,将每个item的mShiftingMode设为false

@SuppressLint("RestrictedApi")
    private void disableShiftMode(){
        BottomNavigationMenuView menuView = (BottomNavigationMenuView) bottomNavigationView.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 itemView = (BottomNavigationItemView) menuView.getChildAt(i);
                itemView.setShiftingMode(false);
                itemView.setChecked(itemView.getItemData().isChecked());
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

声明上述方法并调用,再运行程序,就可以实现如开头所展现的效果了。


github完整示例:https://github.com/WeekL/BottomNavigationViewDemo