【Android】Material Design 之二 BottomNavigationView使用

上午记录了TabLayout的使用,简单实现了一个顶部可滑动的导航效果,突然想到Material Design的另一个控件BottomNavigationView,可以实现类似淘宝、微信、QQ、京东的底部导航栏的效果,下面就来介绍一下使用BottomNavigationView来实现底部导航栏的效果。

使用该控件同样需要添加Material Design的依赖:(运行环境是在Android Studio 3.0)

implementation 'com.android.support:design:28.0.0-alpha1'

因为BottomNavigationView控件是通过app:menu属性,使用Menu的形式为底部导航栏指定元素的,所以第一步就要新建一个菜单xml文件,在menu文件夹下新建bottom_navigation_view.xml,布局内容如下:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@+id/item_home"
        android:icon="@drawable/ic_home_grey"
        android:title="首页"/>

    <item android:id="@+id/item_music"
        android:icon="@drawable/ic_music_grey"
        android:title="音乐"/>

    <item android:id="@+id/item_find"
        android:icon="@drawable/ic_find_grey"
        android:title="发现"/>
    
</menu>

 

 BottomNavigationView一般也是和ViewPager+Fragment搭配使用,所以第二步就写下布局文件,

activity_bottom_navigation_view.xml布局内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".BottomNavigationViewActivity">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
    </android.support.v4.view.ViewPager>

    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#6b6b6b"/>
    
    <android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:menu="@menu/bottom_navigation_view">

    </android.support.design.widget.BottomNavigationView>
</LinearLayout>

 创建BottonNaviFragment继承自Fragment,其布局文件fragment_bottom_navi.xml文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:textColor="#000000"
        android:id="@+id/tv_content"/>
</LinearLayout>

BottonNaviFragment.java文件如下: 

@SuppressLint("ValidFragment")
public class BottonNaviFragment extends Fragment {

    private TextView textView;
    private String title;

    public BottonNaviFragment(String title) {
        this.title = title;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view=inflater.inflate(R.layout.fragment_bottom_navi,container,false);
        textView=view.findViewById(R.id.tv_content);
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        textView.setText(title);
    }
}

 创建Fragment适配器文件FragmentAdapter.java文件如下:

public class FragmentAdapter extends FragmentPagerAdapter {

    private List<Fragment> list;     //存放ViewPager中要填充的Fragment

    public FragmentAdapter(FragmentManager fm,List<Fragment> list) {
        super(fm);
        this.list=list;
    }


    @Override
    public Fragment getItem(int i) {
        return list.get(i);
    }

    @Override
    public int getCount() {
        return list.size();
    }
}

 BottomNavigationViewActivity.java文件如下:

public class BottomNavigationViewActivity extends AppCompatActivity {

    private ViewPager viewPager;
    private BottomNavigationView bottomNavigationView;
    private MenuItem menuItem;  //菜单子项

    private List<Fragment> list; 
    private FragmentAdapter fragmentAdapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bottom_navigation_view);

        initView();
        initData();
    }

    private void initView() {
        viewPager=findViewById(R.id.viewpager);
        bottomNavigationView=findViewById(R.id.bottom_navigation_view);
        
        //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){
                    menuItem.setChecked(false);
                }else{
                    bottomNavigationView.getMenu().getItem(0).setChecked(false);
                }
                menuItem=bottomNavigationView.getMenu().getItem(position);
                menuItem.setChecked(true);
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });

        //bottmNavigationView菜单选择监听
        bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                switch(item.getItemId()){
                    case R.id.item_home:
                        viewPager.setCurrentItem(0);
                        break;
                    case R.id.item_music:
                        viewPager.setCurrentItem(1);
                        break;
                    case R.id.item_find:
                        viewPager.setCurrentItem(2);
                        break;
                }
                return false;
            }
        });
    }

    private void initData() {
        list=new ArrayList<>();
        list.add(new BottonNaviFragment("首页"));
        list.add(new BottonNaviFragment("音乐"));
        list.add(new BottonNaviFragment("发现"));

        fragmentAdapter=new FragmentAdapter(getSupportFragmentManager(),null,list);
        viewPager.setAdapter(fragmentAdapter);
    }
}

到此运行下项目,效果如图:

 默认元素选中时图标、文字的颜色为@color/colorPrimary,如果我们想改变导航栏中图标、文字在选中和未选中时的颜色,可以通过BottomNavigationView控件的两个属性去实现,分别是

app:itemTextColor=""
app:itemIconTint=""

 为了方便效果展示,在这里我们设定图标、文字在选中时颜色为红色,未选中时为黑色,涉及到颜色选择,需要在color文件夹下新建一个颜色选择器bottomnavigation_select.xml文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:color="#ff2200" android:state_checked="true"/>
    <item android:color="#000000"/>

</selector>

 然后在activity_bottom_navigation_view.xml中的BottomNavigationView控件下,增加属性:

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

运行一下,效果如图:

 

 以上是导航栏只有3个元素时效果,下面将元素增加到4个,运行效果如图:

从图中可以发现,当导航栏中元素增加到4个时,效果就不一样了,只有当元素选中以及滑动到对应元素时,文字才会出现,未选中时,文字是隐藏的。 这是因为官方的BottomNavigationView默认有个放大的ShiftingMode效果,但是尚未支持代码层级的切换。在3个元素及以下时是默认关闭的,而到了4个及以上时就会开启ShiftingMode效果,并且没有任何属性和方法去修改ShiftingMode,此时我们只能通过反射来修改:

新建一个BottomNavigationView的帮助者类BottomNavigationViewHelper.java,代码如下:

import android.support.design.internal.BottomNavigationItemView;
import android.support.design.internal.BottomNavigationMenuView;
import android.support.design.widget.BottomNavigationView;
import android.util.Log;

import java.lang.reflect.Field;

/**
 * 新建一个BottomNavigationview帮助者类,
 * 通过反射来修改ShiftingMode
 */
public class BottomNavigationViewHelper {
    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);
                //noinspection RestrictedApi
                item.setShiftingMode(false);
                // set once again checked value, so view will be updated
                //noinspection RestrictedApi
                item.setChecked(item.getItemData().isChecked());
            }
        } catch (NoSuchFieldException e) {
            Log.e("BNVHelper", "Unable to get shift mode field", e);
        } catch (IllegalAccessException e) {
            Log.e("BNVHelper", "Unable to change value of shift mode", e);
        }
    }
}

 然后在 BottomNavigationViewActivity.java中调用BottomNavigationViewHelper的静态方法disableShiftMode()即可。

bottomNavigationView=findViewById(R.id.bottom_navigation_view);
BottomNavigationViewHelper.disableShiftMode(bottomNavigationView);

当你的build.gradle中 依赖库 'com.android.support:appcompat-v7:X.0.0-rc02' 中X小于28时,以上代码是没有问题的,当X等于28时(我使用的是Android Studio 3.0,依赖包是implementation 'com.android.support:appcompat-v7:28.0.0-rc02'),item.setShiftingMode(false)就会报Cannot resolve method 'setShiftingMode(Boolean)'的错误,借鉴了

https://stackoverflow.com/questions/51342200/cannot-resolve-method-setshiftingmodeboolean-in-bottomnavigationview的解决方式,将BottomNavigationViewHelper.java代码修改如下:

public class BottomNavigationViewHelper {
    @SuppressLint("RestrictedApi")
    public static void removeNavigationShiftMode(BottomNavigationView view) {
        BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
        menuView.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED);
        menuView.buildMenuView();
    }
}

 同样的,然后在 BottomNavigationViewActivity.java中调用BottomNavigationViewHelper的静态方法removeNavigationShiftMode()

bottomNavigationView=findViewById(R.id.bottom_navigation_view);
BottomNavigationViewHelper.removeNavigationShiftMode(bottomNavigationView);

 一番修改之后,再运行下项目,看下效果:

 此时,4个元素的效果和3个元素的效果就一样啦。

最后关于BottomNavigationView控件作点补充:

1、如果要设置底部导航栏的背景颜色,可以通过BottomNavigationView的属性app:itemBackground来设置,默认是当前主题的背景色,白色or黑色。

2、官方建议导航栏元素item个数为3-5个 ,最多5个,如果设置6个会直接报错。设置2个则不会报错,但是如果是2个的话不建议使用该控件。

3、如果想实现元素是不带文字的图标,可以不设置菜单的title值,例如,不设置“发现”的元素文字,运行后效果如下方:

好啦,关于BottomNavigationView实现底部导航栏的使用就介绍到这里啦。