最近開(kāi)展新的項(xiàng)目滔以,首當(dāng)其中的問(wèn)題就是首頁(yè)導(dǎo)航。
雖然之前就已經(jīng)知道BottomNavigationView
的存在氓拼,但是一直沒(méi)有使用你画。原因也很明顯,BottomNavigationView
存在兩個(gè)非常嚴(yán)重的問(wèn)題:
- 使用png格式的圖標(biāo)時(shí)無(wú)法顯示原本的圖案桃漾;
-
默認(rèn)有位移動(dòng)畫(huà)坏匪,且無(wú)法通過(guò)配置進(jìn)行取消。
而這次不想再次進(jìn)行一次自定義撬统,決定采用官方的BottomNavigationView
适滓。那么就要解決上面提到的兩個(gè)問(wèn)題。
本著不重復(fù)造輪子的原則(主要是懶)恋追,先搜索了一下這兩個(gè)問(wèn)題的解決方案凭迹。
其中無(wú)法顯示png圖標(biāo)的問(wèn)題可以完美解決罚屋。但是去除位移動(dòng)畫(huà)的處理都不完美,并且全網(wǎng)的答案驚人一致嗅绸。問(wèn)題如下:
僅解決了位移動(dòng)畫(huà)的問(wèn)題脾猛,但是并未解決圖標(biāo)及文本變大的問(wèn)題。
沒(méi)有時(shí)間看完下面改造過(guò)程的鱼鸠,直接復(fù)制這個(gè)文件添加到項(xiàng)目中即可:BottomNavigationViewExtension猛拴。
禁用位移動(dòng)畫(huà)
要禁用位移動(dòng)畫(huà),首先要找到使位移動(dòng)畫(huà)生效的代碼蚀狰。我們定位到BottomNavigationView
愉昆,分析代碼后發(fā)現(xiàn)該View實(shí)際上是對(duì)BottomNavigationMenuView
的包裝:
// ... 無(wú)關(guān)代碼省略
private final BottomNavigationMenuView mMenuView;
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Create the menu
mMenu = new BottomNavigationMenu(context);
mMenuView = new BottomNavigationMenuView(context);
// 將一個(gè)BottomNavigationMenuView添加到了當(dāng)前View中
addView(mMenuView, params);
}
則位移動(dòng)畫(huà)實(shí)際生效應(yīng)該在BottomNavigationMenuView
中,那么我們繼續(xù)分析BottomNavigationMenuView
的代碼:
public void buildMenuView() {
// ...
// 無(wú)關(guān)代碼省略
mButtons = new BottomNavigationItemView[mMenu.size()];
mShiftingMode = mMenu.size() > 3; // 當(dāng)Menu中的item數(shù)量>3時(shí)麻蹋,默認(rèn)開(kāi)啟了位移動(dòng)畫(huà)
for (int i = 0; i < mMenu.size(); i++) {
// ...
// 無(wú)關(guān)代碼省略
BottomNavigationItemView child = getNewItem();
child.setShiftingMode(mShiftingMode);
addView(child);
}
}
我們發(fā)現(xiàn)跛溉,在菜單中條目的數(shù)量大于3時(shí),開(kāi)啟了位移動(dòng)畫(huà)(mShiftingMode=true)倒谷。所以,如果要禁用位移動(dòng)畫(huà)的話(huà)深夯,需要將mShiftingMode
的值設(shè)置為false诺苹,并且重建菜單收奔。
但是mShiftingMode
的賦值是在重建菜單的時(shí)候進(jìn)行的,在不修改源碼的情況下此處無(wú)法做出修改质蕉。故繼續(xù)分析BottomNavigationItemView
模暗。
@Override
public void setChecked(boolean checked) {
// ... 省略無(wú)關(guān)代碼
if (mShiftingMode) {
// 當(dāng)mShiftingMode=true時(shí)兑宇,該分支生效
if (checked) {
// ... 省略無(wú)關(guān)代碼
mLargeLabel.setVisibility(VISIBLE);
mLargeLabel.setScaleX(1f);
mLargeLabel.setScaleY(1f);
} else {
// ... 省略無(wú)關(guān)代碼
mLargeLabel.setVisibility(INVISIBLE);
mLargeLabel.setScaleX(0.5f);
mLargeLabel.setScaleY(0.5f);
}
mSmallLabel.setVisibility(INVISIBLE);
} else {
// 當(dāng)mShiftingMode=false時(shí)隶糕,該分支生效
if (checked) {
LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
// 選中時(shí),圖標(biāo)的頂部外邊距增加了mDefaultMargin+mShiftAmount
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);
}
}
refreshDrawableState();
}
從上述代碼分析,要禁用位移動(dòng)畫(huà)测秸,需要將BottomNavigationItemView.mShiftingMode
的值設(shè)置為false
霎冯。如果想要不修改源碼,則此處需要使用反射慷荔。為了使用方便显晶,定義一個(gè)BottomNavigationView
的擴(kuò)展方法:
fun BottomNavigationView.disableShiftMode() {
try {
val bottomNavigationMenuView = getChildAt(0) as BottomNavigationMenuView
// BottomNavigationMenuView未提供setter方法來(lái)控制位移動(dòng)畫(huà)的開(kāi)關(guān)磷雇,在不修改源碼的前提下唯笙,只能通過(guò)反射來(lái)實(shí)現(xiàn)
val shiftingMode = bottomNavigationMenuView.javaClass.getDeclaredField("mShiftingMode")
shiftingMode.setBooleanValue(bottomNavigationMenuView, false)
val childCount = bottomNavigationMenuView.childCount
for (i in 0 until childCount) {
val bottomNavigationItemView = bottomNavigationMenuView.getChildAt(
i) as BottomNavigationItemView
bottomNavigationItemView.setShiftingMode(false)
// 重建菜單崩掘,該方法限制為進(jìn)在com.android.support包中才能夠調(diào)用,不知何時(shí)就會(huì)被隱藏掉.在被隱藏掉的時(shí)候可以嘗試采用反射的方式進(jìn)行調(diào)用
bottomNavigationMenuView.updateMenuView()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun Field.setBooleanValue(obj: Any, value: Boolean) {
isAccessible = true
setBoolean(obj, value)
isAccessible = false
}
注意:BottomNavigationMenuView.updateMenuView()
和BottomNavigationItemView.setShiftingMode()
被限制為僅能在com.android.support
包中調(diào)用苞慢,如果在將來(lái)的support包中將這兩個(gè)方法私有化英妓,則可以嘗試通過(guò)反射的方式來(lái)調(diào)用蔓纠。
修改完畢后效果如下:
統(tǒng)一所有條目中圖標(biāo)邊距和文本的大小
在禁用了位移動(dòng)畫(huà)后贺纲,我們發(fā)現(xiàn)在選中一個(gè)條目的時(shí)候,條目的圖標(biāo)和文本框還是有輕微的位移侮措。我們繼續(xù)向下分析:
@Override
public void setChecked(boolean checked) {
// ... 省略無(wú)關(guān)代碼
if (mShiftingMode) {
// 當(dāng)mShiftingMode=true時(shí)乖杠,該分支生效
if (checked) {
// ... 省略無(wú)關(guān)代碼
mLargeLabel.setVisibility(VISIBLE);
mLargeLabel.setScaleX(1f);
mLargeLabel.setScaleY(1f);
} else {
// ... 省略無(wú)關(guān)代碼
mLargeLabel.setVisibility(INVISIBLE);
mLargeLabel.setScaleX(0.5f);
mLargeLabel.setScaleY(0.5f);
}
mSmallLabel.setVisibility(INVISIBLE);
} else {
// 當(dāng)mShiftingMode=false時(shí),該分支生效
if (checked) {
LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
// 選中時(shí)胧洒,圖標(biāo)的頂部外邊距增加了mDefaultMargin+mShiftAmount
iconParams.topMargin = mDefaultMargin + mShiftAmount;
mIcon.setLayoutParams(iconParams);
// 選中時(shí)卫漫,mLargeLabel顯示列赎、mSmallLabel隱藏
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;
// 未選中時(shí),圖標(biāo)的頂部外邊距為默認(rèn)值
iconParams.topMargin = mDefaultMargin;
mIcon.setLayoutParams(iconParams);
// 未選中時(shí)饼煞,mLargeLabel隱藏砖瞧、mSmallLabel顯示
mLargeLabel.setVisibility(INVISIBLE);
mSmallLabel.setVisibility(VISIBLE);
mLargeLabel.setScaleX(mScaleDownFactor);
mLargeLabel.setScaleY(mScaleDownFactor);
mSmallLabel.setScaleX(1f);
mSmallLabel.setScaleY(1f);
}
}
refreshDrawableState();
}
從上面的代碼中可以看出嚷狞,問(wèn)題原因在于選中的條目的圖標(biāo)增加了頂部外邊距mShiftAmount
感耙,以及大小兩個(gè)文本框的顯隱變化即硼。
為了解決這兩個(gè)問(wèn)題屡拨,做出如下擴(kuò)展:
/**
* 統(tǒng)一每個(gè)條目的文本大小和圖標(biāo)的外邊距
*/
fun BottomNavigationView.unifyItems(forceUpdate: Boolean = true) {
try {
val bottomNavigationMenuView = getChildAt(0) as BottomNavigationMenuView
val childCount = bottomNavigationMenuView.childCount
for (i in 0 until childCount) {
val child = bottomNavigationMenuView.getChildAt(i) as BottomNavigationItemView
val clazz = child.javaClass
// 使選中/未選中條目的頂部邊距不發(fā)生變化
val shiftAmountField = clazz.getDeclaredField("mShiftAmount")
shiftAmountField.setIntValue(child, 0)
// 使選中/未選中條目的文本保持同樣的大小
child.unifyTextSize()
}
if (forceUpdate) {
bottomNavigationMenuView.updateMenuView()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun BottomNavigationItemView.unifyTextSize() {
val baselineLayout = getChildAt(1) as BaselineLayout
val smallLabel = baselineLayout.getChildAt(0) as TextView
val largeLabel = baselineLayout.getChildAt(1) as TextView
largeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallLabel.textSize)
}
private fun Field.setIntValue(obj: Any, value: Int) {
isAccessible = true
setInt(obj, value)
isAccessible = false
}
效果如下:
顯示圖標(biāo)原本圖案
/**
* 禁用`app:itemIconTint`屬性,讓[BottomNavigationView]的圖標(biāo)顯示原本的顏色
*/
fun BottomNavigationView.disableIconTint() {
this.itemIconTintList = null
}
最終效果如下: