Android Design 28 BottomNavigationView 的使用

image.png
前言
- 之前專案中一直使用 BottomNavigationView 來做首頁導航,最近 Google 的支援庫 28 版本釋出了,立馬就把支援庫更新到了最新版本,然後就是一堆報錯,發現很多在支援庫上進行自定義的功能都出現了問題,╮(╯_╰)╭ 而其中最重要的首頁導航已經無法實現之前的效果了,因此有了這篇文章。
BottomNavigationView 的使用
- 在說明 28 版本上的處理之前,我們還是先來簡單的說一下 BottomNavigationView 的使用吧。
- 首先在專案中匯入 Design 支援庫
implementation 'com.android.support:design:27.1.1'
- 這裡使用的還是舊版本,在佈局中使用
<android.support.design.widget.BottomNavigationView android:id="@+id/bnv" android:layout_width="match_parent" android:layout_height="@dimen/app_spacing_multiple_12" android:background="@color/white" android:bind_bnv_onItemSelected="@{viewModel.command.onItemSelected}" android:textSize="@dimen/app_text_size_14" app:itemIconTint="@color/app_selector_main_bottom_tint" app:itemTextColor="@color/app_selector_main_bottom_tint" app:menu="@menu/app_menu_main_bottom" />
- 選單資源
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_hospital" android:icon="@drawable/app_selector_main_bottom_hospital" android:title="@string/app_main_bottom_hospital" /> <item android:id="@+id/menu_consult" android:icon="@drawable/app_selector_main_bottom_consult" android:title="@string/app_main_bottom_consult" /> <item android:id="@+id/menu_contacts" android:icon="@drawable/app_selector_main_bottom_contacts" android:title="@string/app_main_bottom_contacts" /> <item android:id="@+id/menu_setting" android:icon="@drawable/app_selector_main_bottom_setting" android:title="@string/app_main_bottom_setting" /> </menu>

螢幕快照 2018-10-11 15.55.10.png
- 然後實現出來的是這種效果,如果想要像普通的導航欄一樣使用呢?這時候你需要用的反射。
/** * 關閉切換動畫效果 * - library 28 以下使用 */ @SuppressLint("RestrictedApi") fun BottomNavigationView.disableShiftMode() { val menuView = this.getChildAt(0) as BottomNavigationMenuView try { val shiftingMode = menuView.javaClass.getDeclaredField("mShiftingMode") shiftingMode.isAccessible = true shiftingMode.setBoolean(menuView, false) shiftingMode.isAccessible = false for (i in 0 until menuView.childCount) { val item = menuView.getChildAt(i) as BottomNavigationItemView item.setShiftingMode(false) item.setChecked(item.itemData.isChecked) } } catch (e: NoSuchFieldException) { Log.e("Common_BottomNavigation", "Unable to get shift mode field", e) } catch (e: IllegalAccessException) { Log.e("Common_BottomNavigation", "Unable to change value of shift mode", e) } }
- 沒錯,使用反射來修改對應屬性,來實現我們想要的樣式。程式碼中使用:
mNavigation.disableShiftMode()

螢幕快照 2018-10-11 15.58.48.png
- 好了,關於 BottomNavigationView 的使用就講這麼多,想要知道更詳細的使用方法可以去百度、Google,今天的重點不在這裡。
Design 28 中的變化
- 接下來就說到重點了,我們把依賴升級到 28,像這樣
implementation 'com.android.support:design:28.0.0'
-
然後你就會發現,之前定義的 disableShiftMode() 方法報錯了。
螢幕快照 2018-10-11 16.05.04.png
-
甚至你在 BottomNavigationMenuView 裡面都找不到 mShiftingMode 這個變數。
-
那麼我們要怎麼實現上面使用的樣式呢?
-
我們知道,使用 BottomNavigationView 的時候,如果 item 的個數小於 4 個,就是我們需要的樣式,這裡我們檢視 BottomNavigationView 的程式碼,查詢哪裡有和 item 的數量有關,和數字 3 或者 4 有關。
BottomNavigationMenuView.class private boolean isShifting(int labelVisibilityMode, int childCount) { return labelVisibilityMode == -1 ? childCount > 3 : labelVisibilityMode == 0; }
- 我們找到了 BottomNavigationMenuView 裡面的程式碼,從這裡我們可以看到,如果 item 數量為 3,也就是說
labelVisibilityMode == -1
且childCount == 3
時,這個時候,顯示的是我們需要的樣式,再查詢 labelVisibilityMode 在哪裡被使用。
BottomNavigationItemView.class public void setChecked(boolean checked) { this.largeLabel.setPivotX((float)(this.largeLabel.getWidth() / 2)); this.largeLabel.setPivotY((float)this.largeLabel.getBaseline()); this.smallLabel.setPivotX((float)(this.smallLabel.getWidth() / 2)); this.smallLabel.setPivotY((float)this.smallLabel.getBaseline()); switch(this.labelVisibilityMode) { case -1: if (this.isShifting) { if (checked) { this.setViewLayoutParams(this.icon, this.defaultMargin, 49); this.setViewValues(this.largeLabel, 1.0F, 1.0F, 0); } else { this.setViewLayoutParams(this.icon, this.defaultMargin, 17); this.setViewValues(this.largeLabel, 0.5F, 0.5F, 4); } this.smallLabel.setVisibility(4); } else if (checked) { this.setViewLayoutParams(this.icon, (int)((float)this.defaultMargin + this.shiftAmount), 49); this.setViewValues(this.largeLabel, 1.0F, 1.0F, 0); this.setViewValues(this.smallLabel, this.scaleUpFactor, this.scaleUpFactor, 4); } else { this.setViewLayoutParams(this.icon, this.defaultMargin, 49); this.setViewValues(this.largeLabel, this.scaleDownFactor, this.scaleDownFactor, 4); this.setViewValues(this.smallLabel, 1.0F, 1.0F, 0); } break; case 0: if (checked) { this.setViewLayoutParams(this.icon, this.defaultMargin, 49); this.setViewValues(this.largeLabel, 1.0F, 1.0F, 0); } else { this.setViewLayoutParams(this.icon, this.defaultMargin, 17); this.setViewValues(this.largeLabel, 0.5F, 0.5F, 4); } this.smallLabel.setVisibility(4); break; case 1: if (checked) { this.setViewLayoutParams(this.icon, (int)((float)this.defaultMargin + this.shiftAmount), 49); this.setViewValues(this.largeLabel, 1.0F, 1.0F, 0); this.setViewValues(this.smallLabel, this.scaleUpFactor, this.scaleUpFactor, 4); } else { this.setViewLayoutParams(this.icon, this.defaultMargin, 49); this.setViewValues(this.largeLabel, this.scaleDownFactor, this.scaleDownFactor, 4); this.setViewValues(this.smallLabel, 1.0F, 1.0F, 0); } break; case 2: this.setViewLayoutParams(this.icon, this.defaultMargin, 17); this.largeLabel.setVisibility(8); this.smallLabel.setVisibility(8); } this.refreshDrawableState(); this.setSelected(checked); }
- 當
labelVisibilityMode == -1
且childCount == 3
時,isShifting == false
,也就是說,執行的程式碼是
if (checked) { this.setViewLayoutParams(this.icon, (int)((float)this.defaultMargin + this.shiftAmount), 49); this.setViewValues(this.largeLabel, 1.0F, 1.0F, 0); this.setViewValues(this.smallLabel, this.scaleUpFactor, this.scaleUpFactor, 4); } else { this.setViewLayoutParams(this.icon, this.defaultMargin, 49); this.setViewValues(this.largeLabel, this.scaleDownFactor, this.scaleDownFactor, 4); this.setViewValues(this.smallLabel, 1.0F, 1.0F, 0); }
- 而我們會看到在下方,當
labelVisibilityMode == 1
時,執行的程式碼和labelVisibilityMode == -1
且childCount == 3
是一樣的,也就是說,如果labelVisibilityMode == 1
,就能實現我們需要的效果。 - 接下來我們就可以查詢 labelVisibilityMode 這個引數是在哪裡設定的。
BottomNavigationView.class this.setLabelVisibilityMode(a.getInteger(styleable.BottomNavigationView_labelVisibilityMode, -1));
- 沒錯,Google 專門為這種效果添加了對應的屬性 ┭┮﹏┭┮
- 我們可以檢視支援庫中的資原始檔
<attr name="labelVisibilityMode"> <enum name="auto" value="-1"/> <enum name="selected" value="0"/> <enum name="labeled" value="1"/> <enum name="unlabeled" value="2"/> </attr>
- 沒錯,就是他了,我們再在佈局中使用
<android.support.design.widget.BottomNavigationView android:id="@+id/bnv" android:layout_width="match_parent" android:layout_height="@dimen/app_spacing_multiple_12" android:background="@color/white" android:bind_bnv_onItemSelected="@{viewModel.command.onItemSelected}" android:textSize="@dimen/app_text_size_14" app:itemIconTint="@color/app_selector_main_bottom_tint" app:itemTextColor="@color/app_selector_main_bottom_tint" app:labelVisibilityMode="labeled" app:menu="@menu/app_menu_main_bottom" />
-
完美解決 (づ。◕‿‿◕。)づ
螢幕快照 2018-10-11 15.58.48.png
總結
- 所以,支援庫低於 28 的時候使用 BottomNavigationView ,我們可能需要利用反射來處理相關屬性,升級到 28 之後,就可以更簡單的使用了,只用新增
app:labelVisibilityMode="labeled"
就可以了。