Android Support Library 25.0.0 版本中挥唠,新增加了一個(gè)API –> BottomNavigationView – 底部導(dǎo)航視圖。
先來看看這個(gè)控件的實(shí)現(xiàn)效果。
基本使用
使用起來也很簡單
首先在xml中引入該控件
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_navi_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:itemBackground="@android:color/white"
app:menu="@menu/menu_bottom_navi" />
該控件的基本屬性有:
app:itemIconTint : 設(shè)置菜單圖標(biāo)著色
app:itemTextColor : 設(shè)置菜單文本顏色
app:menu : 設(shè)置菜單
app:itemBackground : 設(shè)置導(dǎo)航欄的背景色
@menu/menu_buttom_navi
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_recent"
android:icon="@drawable/ic_history_black_24dp"
android:title="@string/menu_recents" />
<item
android:id="@+id/menu_favorites"
android:icon="@drawable/ic_favorite_black_24dp"
android:title="@string/menu_favorites" />
<item
android:id="@+id/menu_nearby"
android:icon="@drawable/ic_place_black_24dp"
android:title="@string/menu_nearby" />
<item
android:id="@+id/menu_navi"
android:icon="@drawable/ic_navigation_black_24dp"
android:title="@string/menu_navigation" />
</menu>
與定義普通menu布局一樣。
接下來是Java代碼
bottomNaviView = (BottomNavigationView) findViewById(R.id.bottom_navi_view);
bottomNaviView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_recent:
break;
case R.id.menu_favorites:
break;
case R.id.menu_nearby:
break;
case R.id.menu_navi:
break;
}
return true;
}
});
對BottomNavigationView設(shè)置選擇監(jiān)聽器就可以做一些item切換事件了。
注意事項(xiàng)
- 底部導(dǎo)航欄默認(rèn)高度是56dp
- 菜單只能是3-5個(gè)
源碼分析
BottomNavigationView 有幾個(gè)先關(guān)的重要類
- BottomNavigationView
- BottomNavigationMenu
- BottomNavigationMenuView
- BottomNavigationPresenter
它的設(shè)計(jì)有點(diǎn)類似于開發(fā)中的 MVP模式隙袁。
先來看 BottomNavigationView 的構(gòu)造函數(shù)
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context); //檢測當(dāng)前主題
// 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);
mMenuView.setPresenter(mPresenter);
mMenu.addMenuPresenter(mPresenter);
// Custom attributes
// ...省略若干代碼
if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0)); // 加載menu
}
a.recycle();
addView(mMenuView, params); //
mMenu.setCallback(new MenuBuilder.Callback() { // 設(shè)置監(jiān)聽器
@Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
return mListener != null && mListener.onNavigationItemSelected(item);
}
@Override
public void onMenuModeChange(MenuBuilder menu) {}
});
}
ThemeUtils.checkAppCompatTheme(context) 是用來檢測當(dāng)前主題的痰娱。代碼很簡單弃榨,就是判斷是否有 colorPrimary 屬性。
class ThemeUtils {
private static final int[] APPCOMPAT_CHECK_ATTRS = {
android.support.v7.appcompat.R.attr.colorPrimary
};
static void checkAppCompatTheme(Context context) {
TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS);
final boolean failed = !a.hasValue(0);
if (a != null) {
a.recycle();
}
if (failed) {
throw new IllegalArgumentException("You need to use a Theme.AppCompat theme "
+ "(or descendant) with the design library.");
}
}
}
構(gòu)造函數(shù)中接下來調(diào)用了 inflateMenu()
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, mMenu);
mPresenter.initForMenu(getContext(), mMenu);
mPresenter.setUpdateSuspended(false);
mPresenter.updateMenuView(true);
}
可以看出都是調(diào)用的BottomNavigationPresenter的函數(shù)梨睁。
setUpdateSuspended(true) – 暫停修改menu
setUpdateSuspended(false) — 可以修改menu
在initForMenu()一前一后鲸睛,設(shè)置一個(gè)標(biāo)志來表示當(dāng)前正在操作menu。
重點(diǎn)來看 BottomNavigationPresenter.initForMenu()
@Override
public void initForMenu(Context context, MenuBuilder menu) {
mMenuView.initialize(mMenu);
mMenu = menu;
}
函數(shù)內(nèi)部又是調(diào)用的 BottomNavigationMenuView.initialize()
@Override
public void initialize(MenuBuilder menu) {
mMenu = menu;
if (mMenu == null) return;
if (mMenu.size() > mActiveButton) {
mMenu.getItem(mActiveButton).setChecked(true);
}
}
代碼中就是一些簡單的初始化操作坡贺。
接下來看 BottomNavigationPresenter.updateMenuView(true)
@Override
public void updateMenuView(boolean cleared) {
if (mUpdateSuspended) return;
if (cleared) {
mMenuView.buildMenuView();
} else {
mMenuView.updateMenuView();
}
}
第一次創(chuàng)建Menu時(shí)官辈,調(diào)用的是buildMenuView方法。
BottomNavigationMenuView.buildMenuView()
public void buildMenuView() {
if (mButtons != null) {
for (BottomNavigationItemView item : mButtons) {
sItemPool.release(item);
}
}
removeAllViews();
mButtons = new BottomNavigationItemView[mMenu.size()];
// 當(dāng)menu item大于3個(gè)的時(shí)候遍坟,會(huì)出現(xiàn)縮放動(dòng)畫
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);
// 單個(gè)item -- BottomNavigationItenView 的初始化操作
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
// 設(shè)置點(diǎn)擊事件
child.setOnClickListener(mOnClickListener);
// 添加子視圖
addView(child);
}
}
在 BottomNavigationMenuView類中定義了一個(gè)Pool對象拳亿,用來緩存BottomNavigationItemView對象。
private static final Pools.Pool<BottomNavigationItemView> sItemPool = new Pools.SynchronizedPool<>(5);
通過for循環(huán)創(chuàng)建了nMenu.size() 個(gè)BottomNavigationItemView 對象愿伴。
BottomNavigationItemView child = getNewItem();
mButtons[i] = child;
getNewItem()
private BottomNavigationItemView getNewItem() {
BottomNavigationItemView item = sItemPool.acquire();
if (item == null) {
item = new BottomNavigationItemView(getContext());
}
return item;
}
getNewItem() 類似于 **Message.obtain() **的機(jī)制肺魁。
接著看buildMenuView() ,方法最后面隔节,調(diào)用了 **BottomNavigationItemView.initialize() **鹅经,然后調(diào)用 **addView(child) ** 來添加子item視圖。
@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());
}
看一下 BottomNavigationItemView的構(gòu)造函數(shù)
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);
}
代碼中映射了一個(gè)布局文件
design_bottom_navigation_item.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/design_bottom_navigation_margin"
android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
android:duplicateParentState="true" />
<android.support.design.internal.BaselineLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
android:layout_gravity="bottom|center_horizontal"
android:duplicateParentState="true">
<TextView
android:id="@+id/smallLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/design_bottom_navigation_text_size"
android:duplicateParentState="true" />
<TextView
android:id="@+id/largeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:textSize="@dimen/design_bottom_navigation_active_text_size"
android:duplicateParentState="true" />
</android.support.design.internal.BaselineLayout>
</merge>
該布局文件由系統(tǒng)提供怎诫,包括一個(gè)ImageView和兩個(gè)TextView瘾晃。
當(dāng)菜單項(xiàng)大于3個(gè),切換item時(shí)幻妓,被選中的item會(huì)將largeLabel顯示蹦误,將smallLabel隱藏。然后改變ImageView和TextView的margin值達(dá)到動(dòng)畫效果肉津。
在BottomNavigationMenuView的構(gòu)造函數(shù)中對mAnimationHelper進(jìn)行了實(shí)例化
private final BottomNavigationAnimationHelperBase mAnimationHelper;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mAnimationHelper = new BottomNavigationAnimationHelperIcs();
} else {
mAnimationHelper = new BottomNavigationAnimationHelperBase();
}
當(dāng)版本小于14(Android 4.0)時(shí)胖缤,是沒有動(dòng)畫效果的。
class BottomNavigationAnimationHelperBase {
void beginDelayedTransition(ViewGroup view) {
// Do nothing.
}
}
Android 4.0及之上的版本
class BottomNavigationAnimationHelperIcs extends BottomNavigationAnimationHelperBase {
private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
private final TransitionSet mSet;
BottomNavigationAnimationHelperIcs() {
mSet = new AutoTransition();
mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
mSet.setInterpolator(new FastOutSlowInInterpolator());
TextScale textScale = new TextScale();
mSet.addTransition(textScale);
}
void beginDelayedTransition(ViewGroup view) {
TransitionManager.beginDelayedTransition(view, mSet);
}
}
代碼中定義了文本縮放動(dòng)畫 – TextScale
然后來看 BottomNavigationItemView的點(diǎn)擊事件阀圾。
在初始化這些子itemView的時(shí)候就設(shè)置了onClickListener
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
// 設(shè)置點(diǎn)擊事件
child.setOnClickListener(mOnClickListener);
mOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
final int itemPosition = itemView.getItemPosition();
activateNewButton(itemPosition);
mMenu.performItemAction(itemView.getItemData(), mPresenter, 0);
}
};
activateNewButtom()
private void activateNewButton(int newButton) {
if (mActiveButton == newButton) return;
mAnimationHelper.beginDelayedTransition(this); // 產(chǎn)生動(dòng)畫
mPresenter.setUpdateSuspended(true);
mButtons[mActiveButton].setChecked(false); // 舊的被激活的按鈕切換成未被激活狀態(tài)
mButtons[newButton].setChecked(true); // 將新點(diǎn)擊的按鈕切換到被激活狀態(tài)
mPresenter.setUpdateSuspended(false);
mActiveButton = newButton; // 記錄當(dāng)前被激活按鈕的位置
}
先產(chǎn)生動(dòng)畫哪廓,然后切換被選中和之前選中的item的狀態(tài)。
@Override
public void setChecked(boolean checked) {
mItemData.setChecked(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();
}
mShiftingMode 表示是否偏移值初烘。當(dāng)菜單個(gè)數(shù)大于3時(shí)涡真,為true分俯。