Android Design Support Library 25 中增加了 BottomNavigationView 这个控件。该控件可以为我们很方便提供底部导航,下面我们介绍一下该控件。
1、依赖:build.gradle文件中增加依赖
compile 'com.android.support:design:25.3.1'
2、xml文件:
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:menu="@menu/navigation_items" />
3、menu文件:
4、Item中icon和title颜色设置:
app:itemBackground:设置item的背景,对应setItemBackgroundResource(int resId)方法
app:itemIconTint:设置icon的颜色,默认颜色是:@color/colorPrimary。对应setItemIconTintList(ColorStateList tint)方法
app:itemTextColor:设置文字的颜色,默认颜色是:@color/colorPrimary。对应setIteTextColor(ColorStateList textColor)方法
①、自定义颜色
*选择器:res/color/selector_item_icon_tint
* 设置:
app:itemIconTint="@color/selector_item_icon_tint"
5、事件监听:BottomNavigationView 为我们提供两个监听
①、setOnNavigationItemReselectedListener:当MenuItem被选中情况下,再次点击,会调用该方法,可以让我们处理一些选中后再点击刷新的功能。
bottomNavigationView.setOnNavigationItemReselectedListener(new BottomNavigationView.OnNavigationItemReselectedListener() {
@Override
public void onNavigationItemReselected(@NonNull MenuItem item) {
}
});
②、setOnNavigationItemSelectedListener:当点击新的MenuItem情况下,回调该方法。
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
return true;
}
});
以上两种监听,在实际使用中,如果有一种回调,另一种必然不会有回调。
MenuItem:可以拿到每个item的 itemId、groupId、title、icon等,也可以根据点击事件修改对应item图标、颜色、标题等内容。
6、BottomNavigationView 使用中注意:
①、BottomNavigationView 默认的高是56dp
②、菜单元素只能 1~5(早期版本是3~5),否则会报错。通过源码:BottomNavigationMenu.MAX_ITEM_COUNT = 5
③、底部导航栏背景颜色默认是当前样式的背景色(白色/黑色)
7、解决菜单元素>3效果与<=3效果不同的方案:
①、首先我们看部分源码:
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
// Create the menu
mMenu = new BottomNavigationMenu(context);
mMenuView = new BottomNavigationMenuView(context);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
mMenuView.setLayoutParams(params);
mPresenter.setBottomNavigationMenuView(mMenuView);
mPresenter.setId(MENU_PRESENTER_ID);
mMenuView.setPresenter(mPresenter);
mMenu.addMenuPresenter(mPresenter);
mPresenter.initForMenu(getContext(), mMenu);
// Custom attributes
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.BottomNavigationView, defStyleAttr,
R.style.Widget_Design_BottomNavigationView);
if (a.hasValue(R.styleable.BottomNavigationView_itemIconTint)) {
mMenuView.setIconTintList(
a.getColorStateList(R.styleable.BottomNavigationView_itemIconTint));
} else {
mMenuView.setIconTintList(
createDefaultColorStateList(android.R.attr.textColorSecondary));
}
if (a.hasValue(R.styleable.BottomNavigationView_itemTextColor)) {
mMenuView.setItemTextColor(
a.getColorStateList(R.styleable.BottomNavigationView_itemTextColor));
} else {
mMenuView.setItemTextColor(
createDefaultColorStateList(android.R.attr.textColorSecondary));
}
if (a.hasValue(R.styleable.BottomNavigationView_elevation)) {
ViewCompat.setElevation(this, a.getDimensionPixelSize(
R.styleable.BottomNavigationView_elevation, 0));
}
int itemBackground = a.getResourceId(R.styleable.BottomNavigationView_itemBackground, 0);
mMenuView.setItemBackgroundRes(itemBackground);
if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
}
a.recycle();
addView(mMenuView, params);
if (Build.VERSION.SDK_INT < 21) {
addCompatibilityTopDivider(context);
}
mMenu.setCallback(new MenuBuilder.Callback() {
@Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) {
mReselectedListener.onNavigationItemReselected(item);
return true; // item is already selected
}
return mSelectedListener != null
&& !mSelectedListener.onNavigationItemSelected(item);
}
@Override
public void onMenuModeChange(MenuBuilder menu) {}
});
}
通过构造方法,我们发现BottomNavigationView 采用mvp来实现,当我们细看发现ItemView其实就是BotoomNavigationMenuView,那我们继续看BotoomNavigationMenuView部分源码:
public void buildMenuView() {
removeAllViews();
if (mButtons != null) {
for (BottomNavigationItemView item : mButtons) {
mItemPool.release(item);
}
}
if (mMenu.size() == 0) {
mSelectedItemId = 0;
mSelectedItemPosition = 0;
mButtons = null;
return;
}
mButtons = new BottomNavigationItemView[mMenu.size()];
mShiftingMode = mMenu.size() > 3;
for (int i = 0; i < mMenu.size(); i++) {
mPresenter.setUpdateSuspended(true);
mMenu.getItem(i).setCheckable(true);
mPresenter.setUpdateSuspended(false);
BottomNavigationItemView child = getNewItem();
mButtons[i] = child;
child.setIconTintList(mItemIconTint);
child.setTextColor(mItemTextColor);
child.setItemBackground(mItemBackgroundRes);
child.setShiftingMode(mShiftingMode);
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
child.setOnClickListener(mOnClickListener);
addView(child);
}
mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
mMenu.getItem(mSelectedItemPosition).setChecked(true);
}
通过buildMenuVie方法我们发现mShiftingMode = mMenu.size() > 3 (即 mShiftingMode = true or false),由此得知mShiftingMode 的值通过mMenu.size()的值范围来决定的。在遍历过程中,只有child.setShiftingMode(mShiftingMode )不同。
那么我们思路来了,只要把mShiftingMode值修改就可以达到我们需要效果了。
看代码:
BottomNavigationView bottomNavigationView = (BottomNavigationView)findViewById(R.id.bottom_navigation_view_bottomNavigationView);
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); //设置false 让底部导航的动画效果始终保持 <= 3 的效果。
itemView.setChecked(itemView.getItemData().isChecked());
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
这样就是实现菜单元素大于3个时候效果跟小于三个是一样。
8、BottomNavigationItemView:我们通过上面部分源码发现BottomNavigationView 其实就是多个BottomNavigationItemView 。
我们看看源码:
public class BottomNavigationItemView extends FrameLayout implements MenuView.ItemView {
public static final int INVALID_ITEM_POSITION = -1;
private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
private final int mDefaultMargin;
private final int mShiftAmount;
private final float mScaleUpFactor;
private final float mScaleDownFactor;
private boolean mShiftingMode;
private ImageView mIcon;
private final TextView mSmallLabel;
private final TextView mLargeLabel;
private int mItemPosition = INVALID_ITEM_POSITION;
private MenuItemImpl mItemData;
private ColorStateList mIconTint;
public BottomNavigationItemView(@NonNull Context context) {
this(context, null);
}
public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final Resources res = getResources();
int inactiveLabelSize =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size);
int activeLabelSize = res.getDimensionPixelSize(
R.dimen.design_bottom_navigation_active_text_size);
mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin);
mShiftAmount = inactiveLabelSize - activeLabelSize;
mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize;
mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize;
LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
mIcon = (ImageView) findViewById(R.id.icon);
mSmallLabel = (TextView) findViewById(R.id.smallLabel);
mLargeLabel = (TextView) findViewById(R.id.largeLabel);
}
@Override
public void initialize(MenuItemImpl itemData, int menuType) {
mItemData = itemData;
setCheckable(itemData.isCheckable());
setChecked(itemData.isChecked());
setEnabled(itemData.isEnabled());
setIcon(itemData.getIcon());
setTitle(itemData.getTitle());
setId(itemData.getItemId());
}
public void setItemPosition(int position) {
mItemPosition = position;
}
public int getItemPosition() {
return mItemPosition;
}
public void setShiftingMode(boolean enabled) {
mShiftingMode = enabled;
}
@Override
public MenuItemImpl getItemData() {
return mItemData;
}
@Override
public void setTitle(CharSequence title) {
mSmallLabel.setText(title);
mLargeLabel.setText(title);
setContentDescription(title);
}
@Override
public void setCheckable(boolean checkable) {
refreshDrawableState();
}
@Override
public void setChecked(boolean checked) {
ViewCompat.setPivotX(mLargeLabel, mLargeLabel.getWidth() / 2);
ViewCompat.setPivotY(mLargeLabel, mLargeLabel.getBaseline());
ViewCompat.setPivotX(mSmallLabel, mSmallLabel.getWidth() / 2);
ViewCompat.setPivotY(mSmallLabel, mSmallLabel.getBaseline());
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);
ViewCompat.setScaleX(mLargeLabel, 1f);
ViewCompat.setScaleY(mLargeLabel, 1f);
} else {
LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
iconParams.gravity = Gravity.CENTER;
iconParams.topMargin = mDefaultMargin;
mIcon.setLayoutParams(iconParams);
mLargeLabel.setVisibility(INVISIBLE);
ViewCompat.setScaleX(mLargeLabel, 0.5f);
ViewCompat.setScaleY(mLargeLabel, 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);
ViewCompat.setScaleX(mLargeLabel, 1f);
ViewCompat.setScaleY(mLargeLabel, 1f);
ViewCompat.setScaleX(mSmallLabel, mScaleUpFactor);
ViewCompat.setScaleY(mSmallLabel, 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);
ViewCompat.setScaleX(mLargeLabel, mScaleDownFactor);
ViewCompat.setScaleY(mLargeLabel, mScaleDownFactor);
ViewCompat.setScaleX(mSmallLabel, 1f);
ViewCompat.setScaleY(mSmallLabel, 1f);
}
}
refreshDrawableState();
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mSmallLabel.setEnabled(enabled);
mLargeLabel.setEnabled(enabled);
mIcon.setEnabled(enabled);
if (enabled) {
ViewCompat.setPointerIcon(this,
PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
} else {
ViewCompat.setPointerIcon(this, null);
}
}
@Override
public int[] onCreateDrawableState(final int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mItemData != null && mItemData.isCheckable() && mItemData.isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void setShortcut(boolean showShortcut, char shortcutKey) {
}
@Override
public void setIcon(Drawable icon) {
if (icon != null) {
Drawable.ConstantState state = icon.getConstantState();
icon = DrawableCompat.wrap(state == null ? icon : state.newDrawable()).mutate();
DrawableCompat.setTintList(icon, mIconTint);
}
mIcon.setImageDrawable(icon);
}
@Override
public boolean prefersCondensedTitle() {
return false;
}
@Override
public boolean showsIcon() {
return true;
}
public void setIconTintList(ColorStateList tint) {
mIconTint = tint;
if (mItemData != null) {
// Update the icon so that the tint takes effect
setIcon(mItemData.getIcon());
}
}
public void setTextColor(ColorStateList color) {
mSmallLabel.setTextColor(color);
mLargeLabel.setTextColor(color);
}
public void setItemBackground(int background) {
Drawable backgroundDrawable = background == 0
? null : ContextCompat.getDrawable(getContext(), background);
ViewCompat.setBackground(this, backgroundDrawable);
}
}
通过源码中BottomNavigationItemView的构造方法我们发现:它其实就是icon、smallTextView、largeTextView。实现很简单BottomNavigationView。
9、总结:
BottomNavigationView由于提供方法很少,导致我们在实际开发中没有办法达到指定效果(设置icon大小、title大小、其他选中动画等),我们可以通过BottomNavigationView源码,来定制自己BottomNavigationView。
话不多说,给大家推荐两个比较好点底部导航栏:
①、BottomNavigation:功能比较强大。
②、QQNaviView:高仿QQ底部导航效果