Bottom-Navigation
是谷歌官方发布的android底部状态栏,它的动画效果非常的漂亮,看起来非常的让人赏心悦目。为了能够拥有相同的用户体验,google对它有着严格的设计标准,具体的要求和实例请看:官方文档 。同时,谷歌还推出了BottomNavigationView
来实现这种设计。那下面就来看看BottomNavigationView
是如何实现的。
简单使用
通过BottomNavigationView
的官方文档,我们可以看到,BottomNavigationView
是在version 25.0.0
以后被添加进来的,所以在此之前的版本,要使用就需要添加的包:compile 'com.android.support:design:25.0.0'
。同时,官方还给出了简单的使用实例,这里就不在介绍了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <android.support.design.widget.BottomNavigationView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation" android:layout_width="match_parent" android:layout_height="56dp" android:layout_gravity="start" app:menu="@menu/my_navigation_items" /> res/menu/my_navigation_items.xml: <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/action_search" android:title="@string/menu_search" android:icon="@drawable/ic_search" /> <item android:id="@+id/action_settings" android:title="@string/menu_settings" android:icon="@drawable/ic_add" /> <item android:id="@+id/action_navigation" android:title="@string/menu_navigation" android:icon="@drawable/ic_action_navigation_menu" /> </menu>
这里我们看到,BottomNavigationView
的高度被限定在56dp,这个值是在官方的设计文档中明确要求的,因此当你使用大于56dp的高度时,就会有一部分空白区域流出来,同时也完全不建议使用过小的高度,这样,内部的图标或文字都可能会被裁剪掉部分。
实现原理
BottomNavigationView分析
通过阅读BottomNavigationView
源码,我们看到BottomNavigationView
直接通过继承FrameLayout实现。它里面最重要的有3个对象:MenuBuilder
,BottomNavigationMenuView
,
BottomNavigationPresenter
。从他们的命名上,我们就可以知道,MenuBuilder
主要是创建一个menu,通过xml文件,创建menu后,再讲其中的item的title、icon、id等信息传递给BottomNavigationMenuView
去创建最终我们看到的view,同时,也将view的点击事件通过menu的回调传回到BottomNavigationMenuView
;BottomNavigationPresenter
则主要是进行一些逻辑的操作,比如初始化BottomNavigationMenuView
,更新BottomNavigationMenuView
等;
BottomNavigationMenuView
则是具体我们所看到的view,它通过MenuBuilder
来创建item,同时根据click来进行样式的变化。
除了这三个之外,BottomNavigationView
其他部分都是一些参数的设置和初始化,这边就不再介绍了。
通过上面我们可以看到,所有的一切都是围绕BottomNavigationMenuView
所展开,所以我们重点通过BottomNavigationMenuView
来了解整个流程。
初始化
在BottomNavigationView
的构造方法里,程序在创建完这3个对象后,首先对MenuBuilder
进行初始化:
1 2 3 4 5 6 7 public void inflateMenu(int resId) { mPresenter.setUpdateSuspended(true); getMenuInflater().inflate(resId, mMenu);//初始化menu mPresenter.initForMenu(getContext(), mMenu); mPresenter.setUpdateSuspended(false); mPresenter.updateMenuView(true); }
在初始化menu前,先对BottomNavigationPresenter
进行暂停,同样的事情还出现在BottomNavigationMenuView
初始化各个item和每次进行动画时。这样做可以避免在初始化和动画时同时在进行更新动画而冲突。
初始化MenuBuilder
后,再通过BottomNavigationPresenter
对BottomNavigationMenuView
进行初始化:
1 2 3 4 5 @Override public void initForMenu(Context context, MenuBuilder menu) { mMenuView.initialize(mMenu); mMenu = menu; }
同时进行界面创建:
1 2 3 4 5 6 7 8 9 @Override public void updateMenuView(boolean cleared) { if (mUpdateSuspended) return; if (cleared) { mMenuView.buildMenuView(); } else { mMenuView.updateMenuView(); } }
具体界面创建的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void buildMenuView() { if (mButtons != null) { for (BottomNavigationItemView item : mButtons) { sItemPool.release(item); } } removeAllViews(); mButtons = new BottomNavigationItemView[mMenu.size()]; 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); child.initialize((MenuItemImpl) mMenu.getItem(i), 0); child.setItemPosition(i); child.setOnClickListener(mOnClickListener); addView(child); } }
这里我们看到有一个池sItemPool
,当界面重构时,会把原来已有的BottomNavigationItemView
放到池中,再次创建新界面时又从池中取出,这样做可以减少对象的创建数量。同时,程序会根据menu的item数量创建BottomNavigationItemView
数组,而BottomNavigationItemView
就是显示的每一个菜单按钮。里面有3个控件:
1 2 3 4 5 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);
这些就是一个item所显示的内容。
onMeasure和onLayout
在BottomNavigationMenuView
初始化完成之后,就要对里面的控件进行测量和排列。
在onMeasure方法中,做的主要是两件是:1是对里面每一个BottomNavigationItemView
都进行宽高的测量;2是设置整个BottomNavigationMenuView
的宽高。
第一步的测量还分两种情况,当item的数量大于3个时,mShiftingMode
=true。在这种情况下,选中的item和其他的items的宽度是不一样的,所以程序要先计算出选中的item的宽度,然后根据它计算其他items的宽度;第二种情况是当items的数量<=3个时,每个item的宽度是一样的,所以只需要根据总宽度/items的数量就可以计算出item的宽度。
第二步在测量整个view的宽度时,程序将先前的所有可见的items的宽度加起来作为整个BottomNavigationMenuView
的宽度(目前也没有发现有什么可能会使item不可见)。
onLayout
方法就比较简单,它根据之前计算好的每一个item的宽高,从左往右或从右往左放置每一个item的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int count = getChildCount(); final int width = right - left; final int height = bottom - top; int used = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) { child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height); } else { child.layout(used, 0, child.getMeasuredWidth() + used, height); } used += child.getMeasuredWidth(); } }
点击动画和回调
在初始化BottomNavigationMenuView
时,每一个BottomNavigationItemView
都会添加onClickListener
:
1 2 3 4 5 6 7 8 9 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); } };
关键的代码是后面两句,其中一句是执行点击的动画,最后一句是执行menu点击的回调。那我们分别来看一下。
1 2 3 4 5 6 7 8 9 10 11 12 private void activateNewButton(int newButton) { if (mActiveButton == newButton) return; mAnimationHelper.beginDelayedTransition(this); mPresenter.setUpdateSuspended(true); mButtons[mActiveButton].setChecked(false); mButtons[newButton].setChecked(true); mPresenter.setUpdateSuspended(false); mActiveButton = newButton; }
在这里我们看到,主要的操作就是将原来的BottomNavigationItemView
check设置为false,将点击的设置为true,那我们来看BottomNavigationItemView
的setCheck方法里又做了什么。
setCheck
方法也是整个BottomNavigationItemView
最核心的方法。
1 2 3 4 5 6 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());
首先,它将设置menu的item是否为点击;设置两个文本的动画原点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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);
在有移动的情况下,对选中和非选中都进行动画操作,同时,大文本显示,小文本隐藏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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); }
在不移动情况下,对icon的上距进行变化,同时选中时小文本变大,不选择时大文本变小文本。
在点击回调时,执行mMenu.performItemAction (itemView.getItemData(), mPresenter, 0);
代码,该代码会调用MenuItemImpl
invoke方法,并且最终调用callback回调。