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);
}