BottomNavigationView最新用法
- Android中使用BottomNavigationView早已不稀奇,之前其他博客也都介绍的有用法。
但是,现在网上所出现的使用方法已不符合当今需要
- 方法一 (com.android.support:design 版本不高于 27.0.3) 网上一堆
- 方法二 (com.android.support:design 版本高于 27.0.3) 网上没有任何说明
故,在此特别介绍一下方法二 以供参考
方法一:
此方法仅针对 Gradle 文件引用 com.android.support:design 版本不高于 27.0.3
- ⓵ 对于 Item>3 时的平移及缩放动画的处理
- ⓶ 对于 Item 缩放动画(选中放大图标/文字)
对于 Item>3 时的平移及缩放动画的处理
@SuppressLint("RestrictedApi") public static void disableShiftMode(BottomNavigationView navigationView) { try { BottomNavigationMenuView menuView = (BottomNavigationMenuView) navigationView.getChildAt(0); Field mShiftingMode = menuView.getClass().getDeclaredField("mShiftingMode"); mShiftingMode.setAccessible(true); mShiftingMode.setBoolean(menuView, false); mShiftingMode.setAccessible(false); for (int i = 0; i < menuView.getChildCount(); i++) { BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i); itemView.setShifting(false); itemView.setEnabled(true); itemView.setChecked(itemView.getItemData().isChecked()); } } catch (NoSuchFieldException e) { Log.e("BNVHelper", "Unable to get shift mode field", e); } catch (IllegalAccessException e) { Log.e("BNVHelper", "Unable to change value of shift mode", e); } }
对于 Item 缩放动画(选中放大图标/文字)
@SuppressLint("RestrictedApi") public static void disableItemScale(BottomNavigationView view) { try { BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0); Field mLargeLabelField = menuView.getClass().getDeclaredField("mLargeLabel"); Field mSmallLabelField = menuView.getClass().getDeclaredField("mSmallLabel"); Field mShiftAmountField = menuView.getClass().getDeclaredField("mShiftAmount"); Field mScaleUpFactorField = menuView.getClass().getDeclaredField("mScaleUpFactor"); Field mScaleDownFactorField = menuView.getClass().getDeclaredField("mScaleDownFactor"); mSmallLabelField.setAccessible(true); mLargeLabelField.setAccessible(true); mShiftAmountField.setAccessible(true); mScaleUpFactorField.setAccessible(true); mScaleDownFactorField.setAccessible(true); final float fontScale = view.getResources().getDisplayMetrics().scaledDensity; for (int i = 0; i < menuView.getChildCount(); i++) { BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i); TextView lagerObj = (TextView) mLargeLabelField.get(itemView); TextView smallObj = (TextView) mSmallLabelField.get(itemView); lagerObj.setTextSize(smallObj.getTextSize() / fontScale + 0.5f); mShiftAmountField.set(itemView, 0); mScaleUpFactorField.set(itemView, 1f); mScaleDownFactorField.set(itemView, 1f); itemView.setChecked(itemView.getItemData().isChecked()); } } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } }
然后初始化后调用
// 默认count>3时选中效果会影响ViewPager的滑动切换效果,故利用反射去掉 BottomNavigationViewHelper.disableShiftMode(bottomNavigationView); // 本人没有关闭图标缩放动能,该方法不用 // BottomNavigationViewHelper.disableItemScale(bottomNavigationView);
布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".activity.MainActivity"> <com.xxx.xxxxx.view.NoScrollViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <android.support.design.widget.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="@color/colorAppBackground" app:itemIconTint="@drawable/main_view" app:itemTextColor="@drawable/main_view" app:menu="@menu/main_navigation" /> </LinearLayout>
Menu文件
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_home" android:icon="@drawable/icon_main_home" android:orderInCategory="1" android:title="资讯" /> <item android:id="@+id/menu_product" android:icon="@drawable/icon_main_product" android:orderInCategory="2" android:title="项目" /> <item android:id="@+id/menu_lease" android:icon="@drawable/icon_main_lease" android:orderInCategory="4" android:title="租房" /> <item android:id="@+id/menu_mine" android:icon="@drawable/icon_main_mine" android:orderInCategory="5" android:title="我的" /> </menu>
Selector文件
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/colorPrimary" android:state_checked="true" /> <item android:color="@color/colorSecondary" /> </selector>
效果如图:
☠ But! 在 com.android.support:design 版本高于 27.0.3 之后,这个方法失效,这也是我接下来着重介绍 方法二 的原因
方法二
- 此方法仅针对 Gradle 文件引用 com.android.support:design 版本高于 27.0.3
首先看一下源码
当 com.android.support:design <= 27.0.3 时
BottomNavigationMenuView 中源码
public class BottomNavigationMenuView extends ViewGroup implements MenuView { // ... // 就是这个属性 private boolean mShiftingMode = true; // ... public void buildMenuView() { //... mButtons = new BottomNavigationItemView[mMenu.size()]; // 为什么item大于3的时候,会有缩放 mShiftingMode = mMenu.size() > 3; for (int i = 0; i < mMenu.size(); i++) { BottomNavigationItemView child = getNewItem(); child.setShiftingMode(mShiftingMode); //... } } }
BottomNavigationItemView 中源码
public class BottomNavigationItemView extends FrameLayout implements MenuView.ItemView { private final int mShiftAmount; private final float mScaleUpFactor; private final float mScaleDownFactor; private boolean mShiftingMode; 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 = findViewById(R.id.icon); mSmallLabel = findViewById(R.id.smallLabel); mLargeLabel = findViewById(R.id.largeLabel); } @Override public void setChecked(boolean checked) { 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); mLargeLabel.setScaleX(1f); mLargeLabel.setScaleY(1f); } else { LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams(); iconParams.gravity = Gravity.CENTER; iconParams.topMargin = mDefaultMargin; mIcon.setLayoutParams(iconParams); // ShiftingMode模式下,不选中文字隐藏 mLargeLabel.setVisibility(INVISIBLE); mLargeLabel.setScaleX(0.5f); mLargeLabel.setScaleY(0.5f); } mSmallLabel.setVisibility(INVISIBLE); } else { if (checked) { LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams(); iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; // 通过设置mShiftAmount 的大小 控制图片margin,进而控制图片大小 iconParams.topMargin = mDefaultMargin + mShiftAmount; mIcon.setLayoutParams(iconParams); // 通过设置mLargeLabel和mSmallLabel的显示与隐藏控制文本大小 // 如果把他俩TextSize设置为一样。则就不会变化 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(); } }
当 com.android.support:design > 27.0.3 时
BottomNavigationMenuView 中源码
public class BottomNavigationMenuView extends ViewGroup implements MenuView { // ... // mShiftingMode属性消失,取而代之的是这两个属性 // itemHorizontalTranslationEnabled:是否启用水平移动动画(如果允许图标会有少许水平移动) private boolean itemHorizontalTranslationEnabled; // labelVisibilityMode:标签显示模式(labeled:显示标签,auto:根据标签个数判断是否显示,selected:选中的显示,unlabeled:不显示) private int labelVisibilityMode; // ... public void buildMenuView() { //... if (this.menu.size() == 0) { this.selectedItemId = 0; this.selectedItemPosition = 0; this.buttons = null; } else { this.buttons = new BottomNavigationItemView[this.menu.size()]; boolean shifting = this.isShifting(this.labelVisibilityMode, this.menu.getVisibleItems().size()); for(i = 0; i < this.menu.size(); ++i) { this.presenter.setUpdateSuspended(true); this.menu.getItem(i).setCheckable(true); this.presenter.setUpdateSuspended(false); BottomNavigationItemView child = this.getNewItem(); //... } } //... private boolean isShifting(int labelVisibilityMode, int childCount) { // 这个地方很奇怪,看了BottomNavigationItem中对labelVisibilityMode做了对应处理才明白 // 默认labelVisibilityMode为"auto",也就是"-1",如果Item>3默认开启水平动画,否则关闭水平动画。 // 如果labelVisibilityMode用户主动设置了值,且不为"auto",只在"selected"模式下开启动画 // "labeled"和"unlabeled"两种模式都不开启动画 return labelVisibilityMode == -1 ? childCount > 3 : labelVisibilityMode == 0; } //... }
要想彻底关闭水平滑动动画,前提还要同时满足 itemHorizontalTranslationEnabled = false
//... protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int visibleCount = this.menu.getVisibleItems().size(); int totalCount = this.getChildCount(); int heightSpec = MeasureSpec.makeMeasureSpec(this.itemHeight, 1073741824); int totalWidth; int i; int extra; int i; // 两个条件都不满足才能禁止水平动画 if (this.isShifting(this.labelVisibilityMode, visibleCount) && this.itemHorizontalTranslationEnabled) { View activeChild = this.getChildAt(this.selectedItemPosition); i = this.activeItemMinWidth; if (activeChild.getVisibility() != 8) { activeChild.measure(MeasureSpec.makeMeasureSpec(this.activeItemMaxWidth, -2147483648), heightSpec); i = Math.max(i, activeChild.getMeasuredWidth()); } //... } } //...
BottomNavigationItemView 中对 labelVisibilityMode 也做了对应处理
//... // labelVisibilityMode="auto"时,值为-1 // labelVisibilityMode="selected"时,值为0 // labelVisibilityMode="labeled"时,值为1 // labelVisibilityMode="unlabeled"时,值为2 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); } //...
最终,在新版的 Android Design 中使用 BottomNavigationView 只用在布局文件中添加两个属性就可以达到上图中的效果
- app:itemHorizontalTranslationEnabled=”false”
app:labelVisibilityMode=”labeled”
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".activity.MainActivity"> <com.xxx.xxxxx.view.NoScrollViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <android.support.design.widget.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="@color/colorAppBackground" app:itemHorizontalTranslationEnabled="false" app:itemIconTint="@drawable/main_view" app:itemTextColor="@drawable/main_view" app:labelVisibilityMode="labeled" app:menu="@menu/main_navigation" /> </LinearLayout>
最后,展示一下各个模式下区别
labeled
unlabeled
select
auto
- 如果item>3效果等同于select
- 如果item<=3效果等同于labeled