底部导航栏的实现方式多种多样,可以使用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
然后在layout文件中使用BottomNavigationView,ViewPager是内容主体容器,最下面的View是导航栏顶部的阴影效果
itemIconTint是为图标着色,itemTextColor是标题颜色,这里可以使用了一个selector,让选中的item和未选中的item展现不同颜色
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