底部导航栏的实现方式多种多样,可以使用LineatLayout或者RadioGroup自定义控件,也可以直接使用第三方提供的如BottomNavigationBar、BottomBarLayout这些功能更多的控件。而如果我们只是想实现一个简单的只用来切换页面的底部导航栏,使用自定义控件的方法有一堆设置切换图标、selector之类的步骤太过繁琐,使用第三方的控件又有一种杀鸡用牛刀的感觉,因此我们可以使用官方提供的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已经可以正常使用了,但是运行起来我们会发现,底部导航栏显示的样式并非是我们想要的风格
这是因为当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