底部导航栏的实现方式多种多样,可以使用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