BottomNavigationView 是用来实现底部导航的功能,是在api 26的推出的,是兼容的,而且在android Studio有模板代码,用起来很方便,item可以添加1-5个,但是当item超过3是就会有偏移动画 且选中时文字才会显示,如下效果
但在新版的BottomNavigationView 两个属性就可以解决这个问题 (网上通过反射的解决方案在这个新版本无效,因为此版本BottomNavigationView底层源码修改了,后面会提到)
1.引入依赖 design包要28.0.0以上版本
implementation 'com.android.support:design:28.0.0'
或者引入这个包implementation 'com.google.android.material:material:1.0.0-beta01'
,二者选一个即可, google以后应该会推荐这个包。目前还是bata版
2.设置下面这两个属性就可以解决上面问题
app:itemHorizontalTranslationEnabled="false"
app:labelVisibilityMode="labeled"
app:itemHorizontalTranslationEnabled="false"
禁止item水平平移动画效果,对应的的方法
是setItemHorizontalTranslationEnabled(false)
app:labelVisibilityMode="labeled"
设置图标下面的文字显示,该属性对应的值有auto
、labeled
、selected
、unlabeled
auto 当item小于等于3是,显示文字,item大于3个默认不显示,选中显示文字
labeled 始终显示文字
selected 选中时显示
unlabeled 选中时显示
该属性对应的方法是setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED)
看看源码,这两个属性如何起作用的
看过源码你会发现BottomNavigationView 子父关系如下图所示
BottomNavigationView 构造方法
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final Resources res = getResources();
// 未选中的item最大宽度 96dp
inactiveItemMaxWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_item_max_width);
// 未选中的item最小宽度 56dp
inactiveItemMinWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_item_min_width);
// 选中的item最大宽度 168dp
activeItemMaxWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_active_item_max_width);
// 选中的item最小宽度 96dp
activeItemMinWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_active_item_min_width);
// item 的高度56dp
itemHeight = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_height);
…… code略
// 设置底部文字的显示模式,获取labelVisibilityMode属性的值,默认为LABEL_VISIBILITY_AUTO
setLabelVisibilityMode( a.getInteger( R.styleable.BottomNavigationView_labelVisibilityMode,
LabelVisibilityMode.LABEL_VISIBILITY_AUTO));
// 设置是否水平平移
setItemHorizontalTranslationEnabled(a.getBoolean(
R.styleable.BottomNavigationView_itemHorizontalTranslationEnabled, true));
int itemBackground = a.getResourceId(R.styleable.BottomNavigationView_itemBackground, 0);
menuView.setItemBackgroundRes(itemBackground);
if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
//填充menu
inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
}
…… code略
}
public void inflateMenu(int resId) {
presenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, menu);
presenter.setUpdateSuspended(false);
//该方法会调用BottomNavigationMenuView 的buildMenuView方法,创建BottomNavigationItemView并添加到BottomNavigationMenuView中。
presenter.updateMenuView(true);
}
BottomNavigationMenuView 的测量方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
…… code略
//在老版的中有一个mShiftingMode的Boolean属性来判断,新版的BottomNavigationMenuView 没有这个属性,而是用下面的方式进行判断
if (isShifting(labelVisibilityMode, visibleCount) && itemHorizontalTranslationEnabled) {
// 获取选中的item
final View activeChild = getChildAt(selectedItemPosition);
//
int activeItemWidth = activeItemMinWidth;
if (activeChild.getVisibility() != View.GONE) {
// Do an AT_MOST measure pass on the active child to get its desired width, and resize the
// active child view based on that width
activeChild.measure(
MeasureSpec.makeMeasureSpec(activeItemMaxWidth, MeasureSpec.AT_MOST), heightSpec);
// 测量宽度和最小宽度比较取最大值做选中的宽度
activeItemWidth = Math.max(activeItemWidth, activeChild.getMeasuredWidth());
}
// 未选中的item个数
final int inactiveCount = visibleCount - (activeChild.getVisibility() != View.GONE ? 1 : 0);
// 选中item的宽度最大值,总宽度减去未选择item宽度之和
final int activeMaxAvailable = width - inactiveCount * inactiveItemMinWidth;
// 取可以盛放选中的item的最小宽度值
final int activeWidth =
Math.min(activeMaxAvailable, Math.min(activeItemWidth, activeItemMaxWidth));
// 未选择item的最大宽度值,
final int inactiveMaxAvailable =
(width - activeWidth) / (inactiveCount == 0 ? 1 : inactiveCount);
// 取可以盛放所以未选中的item的最小宽度值
final int inactiveWidth = Math.min(inactiveMaxAvailable, inactiveItemMaxWidth);
// 总宽度减去未选中和选中item 所占宽度之后剩下的额外宽度
int extra = width - activeWidth - inactiveWidth * inactiveCount;
// 把额外的空间分配给每个临时item宽度
for (int i = 0; i < totalCount; i++) {
if (getChildAt(i).getVisibility() != View.GONE) {
tempChildWidths[i] = (i == selectedItemPosition) ? activeWidth : inactiveWidth;
// Account for integer division which sometimes leaves some extra pixel spaces.
// e.g. If the nav was 10px wide, and 3 children were measured to be 3px-3px-3px, there
// would be a 1px gap somewhere, which this fills in.
if (extra > 0) {
tempChildWidths[i]++;
extra--;
}
} else {
tempChildWidths[i] = 0;
}
}
}
} else {
…… code略
}
…… code略
}
/**
* @param labelVisibilityMode label(底部文字)显示的模式
* @param childCount BottomNavigationView的Item数
* @return 改返回值数上面两个参数影响,labelVisibilityMode的默认就是LabelVisibilityMode.LABEL_VISIBILITY_AUTO,所以,
* 默认情况只有item大于三,该方法返回值就是true
*/
private boolean isShifting(@LabelVisibilityMode int labelVisibilityMode, int childCount) {
return labelVisibilityMode == LabelVisibilityMode.LABEL_VISIBILITY_AUTO
? childCount > 3
: labelVisibilityMode == LabelVisibilityMode.LABEL_VISIBILITY_SELECTED;
}
BottomNavigationMenuView 的buildMenuView方法
public void buildMenuView() {
…… code略
//根据菜单个数创建储存BottomNavigationItemView的数组
buttons = new BottomNavigationItemView[menu.size()];
boolean shifting = isShifting(labelVisibilityMode, menu.getVisibleItems().size());
for (int i = 0; i < menu.size(); i++) {
presenter.setUpdateSuspended(true);
menu.getItem(i).setCheckable(true);
presenter.setUpdateSuspended(false);
BottomNavigationItemView child = getNewItem();
buttons[i] = child;
child.setIconTintList(itemIconTint);
child.setIconSize(itemIconSize);
// Set the text color the default, then look for another text color in order of precedence.
child.setTextColor(itemTextColorDefault);
child.setTextAppearanceInactive(itemTextAppearanceInactive);
child.setTextAppearanceActive(itemTextAppearanceActive);
child.setTextColor(itemTextColorFromUser);
if (itemBackground != null) {
child.setItemBackground(itemBackground);
} else {
child.setItemBackground(itemBackgroundRes);
}
//将是否发生位移、文字显示模式等传递给子类
child.setShifting(shifting);
child.setLabelVisibilityMode(labelVisibilityMode);
child.initialize((MenuItemImpl) menu.getItem(i), 0);
child.setItemPosition(i);
child.setOnClickListener(onClickListener);
addView(child);
}
selectedItemPosition = Math.min(menu.size() - 1, selectedItemPosition);
menu.getItem(selectedItemPosition).setChecked(true);
}
接着看一下BottomNavigationItemView 里面的一些重要方法
BottomNavigationItemView 关联的布局
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- icon -->
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="@dimen/design_bottom_navigation_margin"
android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
android:layout_gravity="center_horizontal"
android:contentDescription="@null"
android:duplicateParentState="true"/>
<com.google.android.material.internal.BaselineLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:paddingBottom="10dp"
android:clipToPadding="false"
android:duplicateParentState="true">
<!-- icon 底部默认的文字-->
<TextView
android:id="@+id/smallLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:maxLines="1"
android:textSize="@dimen/design_bottom_navigation_text_size"/>
<!-- icon 选择时显示的文字,默认是隐藏的选中是显示,字号比默认的大-->
<TextView
android:id="@+id/largeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:duplicateParentState="true"
android:maxLines="1"
android:textSize="@dimen/design_bottom_navigation_active_text_size"
android:visibility="invisible"/>
</com.google.android.material.internal.BaselineLayout>
</merge>
BottomNavigationItemView 的setChecked方法
@Override
public void setChecked(boolean checked) {
largeLabel.setPivotX(largeLabel.getWidth() / 2);
largeLabel.setPivotY(largeLabel.getBaseline());
smallLabel.setPivotX(smallLabel.getWidth() / 2);
smallLabel.setPivotY(smallLabel.getBaseline());
// 判断labelVisibilityMode
switch (labelVisibilityMode) {
case LabelVisibilityMode.LABEL_VISIBILITY_AUTO: // auto自动模式
if (isShifting) { //只有LABEL_VISIBILITY_AUTO模式才会对isShifting进行判断
if (checked) {
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
setViewValues(largeLabel, 1f, 1f, VISIBLE); //largeLabel显示,
} else {
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER);
setViewValues(largeLabel, 0.5f, 0.5f, INVISIBLE); //largeLabel隐藏
}
//isShifting的值是BottomNavigationMenuView 的isShifting()方法得到的,如果模式是
// LABEL_VISIBILITY_AUTO且item大于3 ,那么改方法返回值就为true,所有默认情况如果item大
//于3 ,底部的文字是隐藏的,选中的才会显示文字
smallLabel.setVisibility(INVISIBLE);
} else {
if (checked) {
setViewLayoutParams(
icon, (int) (defaultMargin + shiftAmount), Gravity.CENTER_HORIZONTAL | Gravity.TOP);
setViewValues(largeLabel, 1f, 1f, VISIBLE);
setViewValues(smallLabel, scaleUpFactor, scaleUpFactor, INVISIBLE);
} else {
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
setViewValues(largeLabel, scaleDownFactor, scaleDownFactor, INVISIBLE);
setViewValues(smallLabel, 1f, 1f, VISIBLE);
}
}
break;
case LabelVisibilityMode.LABEL_VISIBILITY_SELECTED: //selected 选中模式
if (checked) {
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
setViewValues(largeLabel, 1f, 1f, VISIBLE);
} else {
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER);
setViewValues(largeLabel, 0.5f, 0.5f, INVISIBLE);
}
//不管item有多少,默认都是隐藏的
smallLabel.setVisibility(INVISIBLE);
break;
case LabelVisibilityMode.LABEL_VISIBILITY_LABELED:// labeled 显示模式
if (checked) {
setViewLayoutParams(
icon, (int) (defaultMargin + shiftAmount), Gravity.CENTER_HORIZONTAL | Gravity.TOP);
setViewValues(largeLabel, 1f, 1f, VISIBLE); //显示大文字
setViewValues(smallLabel, scaleUpFactor, scaleUpFactor, INVISIBLE);//隐藏小文字
} else {
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
setViewValues(largeLabel, scaleDownFactor, scaleDownFactor, INVISIBLE);//隐藏大文字
setViewValues(smallLabel, 1f, 1f, VISIBLE);//显示小文字
}
break;
case LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED: //unlabeled 文字不显示
setViewLayoutParams(icon, defaultMargin, Gravity.CENTER);
//大小文字都不显示
largeLabel.setVisibility(GONE);
smallLabel.setVisibility(GONE);
break;
default:
break;
}
refreshDrawableState();
// Set the item as selected to send an AccessibilityEvent.TYPE_VIEW_SELECTED from View, so that
// the item is read out as selected.
setSelected(checked);
}
private void setViewValues(@NonNull View view, float scaleX, float scaleY, int visibility) {
view.setScaleX(scaleX);
view.setScaleY(scaleY);
view.setVisibility(visibility);
}