BottomNavigationView是design包提供的底部导航栏,样子跟市面上常见的底栏差很少,可是点选的时候会带有一点动画效果,放张图:
html
首先保证design包被项目引入java
compile 'com.android.support:design:26.0.0-alpha1'复制代码
以后创建menu资源文件,以上图为例:
在/res/menu/bottom_navigation.xml
中加入以下代码android
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="camera"
android:icon="@drawable/ic_camera_black_24dp"
android:id="@+id/menu_camera"/>
<item android:title="palette"
android:icon="@drawable/ic_palette_black_24dp"
android:id="@+id/menu_palette"/>
<item android:title="security"
android:icon="@drawable/ic_security_black_24dp"
android:id="@+id/menu_security"/>
<item android:title="setting"
android:icon="@drawable/ic_settings_black_24dp"
android:id="@+id/menu_setting"/>
</menu>复制代码
在布局文件中使用BottomNavigationView:算法
<android.support.design.widget.BottomNavigationView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_navigation"
android:background="?android:attr/windowBackground"
android:id="@+id/bottom_navigation_view"/>复制代码
只须要使用app:menu="@menu/bottom_navigation"
把菜单配置进来就能够看到gif中的效果了。BottomNavigationView为咱们提供了几个自定义属性api
以图标着色为例,在res/color/bottom_nav_icon_color.xml
中添加以下代码:数组
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true"
android:color="@color/colorAccent"/>
<item android:state_checked="false"
android:color="@android:color/black"/>
</selector>复制代码
selector中只须要定义state_checked为true/false的item就能够了,BottomNavigationView只会用到这两种状态,因此上述代码会将选中的图标染为colorAccent,未选中染为黑色。itemTextColor 与他的定义方式彻底同样,就不贴代码了。若是对选项的background不满意,能够自行定义drawable,举个例子:
在res/drawable-v21/item_background.xml
中加入以下代码安全
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/holo_red_light">
</ripple>复制代码
在res/drawable/item_background.xml
中加入以下代码bash
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/holo_red_light" />
</shape>复制代码
什么,你问我为何写两套drawable?哈哈哈哈哈哈哈哈哈...嗝架构
以后咱们把写过的资源都配置进去,就能够看到下面这个小可爱啦!app
app:itemIconTint="@color/bottom_nav_icon_color"
app:itemTextColor="@color/bottom_nav_text_color"
app:itemBackground="@drawable/item_background"复制代码
好了我认可这一点也不可爱,并且配置很麻烦,因此这里给出一种稍微简单点的配置方式,但不能像上面那种能够控制那么多细节,大概是这个样子:
res/values/styles.xml
中添加一个style
<style name="MyBottomNavigationStyle" parent="Widget.Design.BottomNavigationView">
//ripple的颜色
<item name="colorControlHighlight">@android:color/holo_red_light</item>
//选中时的颜色
<item name="colorPrimary">@android:color/holo_green_dark</item>
//未选中的颜色
<item name="android:textColorSecondary" >@android:color/black</item>
</style>复制代码
以后将这个style配置进去就行了,代码以下:
<android.support.design.widget.BottomNavigationView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_navigation"
android:theme="@style/MyBottomNavigationStyle"
android:background="?android:attr/windowBackground"
android:id="@+id/bottom_navigation_view"/>复制代码
以后就是点击监听的问题了,看代码
BottomNavigationView navigationView = findViewById(R.id.bottom_navigation_view);
navigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_camera:
break;
case R.id.menu_palette:
break;
}
return true;
}
});复制代码
被点击的menuItem会在onNavigationItemSelected方法中回调,以后根据id作操做就行了,注意,若是此方法返回true,则认为事件被处理,BottomNavigationView将播放选项切换动画,若是返回false,点击以后是没有效果的。
BottomNavigationView 还提供了一个OnNavigationItemReselectedListener用于监听已选中的Item被重复点击的状况,在这种状况下,若是设置了此监听,BottomNavigationView 将不回调OnNavigationItemSelectedListener,好比咱们可使用一个OnNavigationItemReselectedListener的空实现来屏蔽item被重复点击的状况。
最后须要说明的是,BottomNavigationView 支持经过代码的方式切换菜单选项,以上图举例,若是咱们想切换到palette菜单的话:
navigationView.setSelectedItemId(R.id.menu_palette);复制代码
传入选项id便可。
好了下面进入正题。
BottomNavigationView基于Android的Menu框架构建,因此,视图方面,主要角色为MenuView、ItemView两个接口,对应的实现分别是BottomNavigationMenuView、BottomNavigationItemView,一个负责选项视图,一个负责总体布局。数据及交互处理方面,主要角色为Menu(MenuBuilder)、MenuItem、MenuPresenter三个接口,实现类分别为BottomNavigationMenu、MenuItemImpl、BottomNavigationPresenter。他们之间的依赖关系见UML图
其实刚刚并无提到BottomNavigationView,因此咱们从这个类入手,了解一下整个源码的细节。BottomNavigationView的工做很少,主要用于为使用者暴露交互api,好比设置着色、设置背景等等,另一个做用就是建立上面提到的各类角色。咱们来看一下他的构造函数:
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
//一、建立MenuBuilder
mMenu = new BottomNavigationMenu(context);
//二、建立MenuView
mMenuView = new BottomNavigationMenuView(context);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
//wrapContent并居中(BottomNavigationView自己是一个FrameLayout)
mMenuView.setLayoutParams(params);
//三、进行注入
mPresenter.setBottomNavigationMenuView(mMenuView);
mPresenter.setId(MENU_PRESENTER_ID);
mMenuView.setPresenter(mPresenter);
mMenu.addMenuPresenter(mPresenter);
mPresenter.initForMenu(getContext(), mMenu);
// 四、解析xml属性并设置
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.BottomNavigationView, defStyleAttr,
R.style.Widget_Design_BottomNavigationView);
//省略设置各类属性的代码...
//大概操做就是若是没有在xml中配置,就建立默认的
if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
//加载菜单并建立相应View
inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
}
a.recycle();
addView(mMenuView, params);
if (Build.VERSION.SDK_INT < 21) {
//5.0之前的在顶部加一个灰色的View当作阴影
addCompatibilityTopDivider(context);
}
//五、监听菜单点击并向外传递事件
mMenu.setCallback(new MenuBuilder.Callback() {...});
}复制代码
代码不复杂,但有几个细节须要注意一下。第一部分中建立的BottomNavigationMenu是MenuBuilder的子类,继承的目的是为了控制选项数量及设置item的属性,他覆写了父类的addInternal方法:
@Override
protected MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
//数量限制
if (size() + 1 > MAX_ITEM_COUNT) {
throw new IllegalArgumentException(
"Maximum number of items supported by BottomNavigationView is " + MAX_ITEM_COUNT
+ ". Limit can be checked with BottomNavigationView#getMaxItemCount()");
}
stopDispatchingItemsChanged();
final MenuItem item = super.addInternal(group, id, categoryOrder, title);
if (item instanceof MenuItemImpl) {
//设为惟一可点击
((MenuItemImpl) item).setExclusiveCheckable(true);
}
startDispatchingItemsChanged();
return item;
}复制代码
此方法在解析menu文件时被调用。第一句的MAX_ITEM_COUNT
是5,限制选项个数,多了就抛异常,以后注意这一句((MenuItemImpl) item).setExclusiveCheckable(true);
将这个Item设置为惟一可选中的,能够理解为将这个选项设置为单选的。举个例子,对于一个menu group来讲,当某个带有Exclusive标记的Item被点选时,menu框架会自动取消选中其余的带有Exclusive标记的选项,从而达到单选的目的。对应的咱们的BottomNavigationView其实就是这种状况,他在这个方法里将每一个Item设置为ExclusiveCheckable,这样就很方便的实现一个item被checked,另外一个就unchecked的效果了。
你们可能注意到源码中的stopDispatchingItemsChanged()
、startDispatchingItemsChanged()
两个方法了,坦率的讲,我是实在没看出有什么用,你们也不要纠结了,这锅26-alpha版原本背。
而后咱们回来看第三部分,一通注入,不跟源码了,直接解释一下:mPresenter.setBottomNavigationMenuView(mMenuView);
将MenuView注入到presenter中mMenuView.setPresenter(mPresenter);
将presenter注入到MenuView中
这样二者互相持有了。以后是mMenu.addMenuPresenter(mPresenter)
将presenter注入到menu中,最后调用mPresenter.initForMenu(getContext(), mMenu)
将menu注入到presenter中,这样他俩也互相持有了,同时presenter会将menu注入到MenuView中,这样整个流程就结束了,你们能够对照uml图再捋一遍。
第四部分中传入的默认style为R.style.Widget_Design_BottomNavigationView
,源码位置为sdk/extra/android/m2repository/com/android/support/design/26.0.0-alpha1
。解压aar文件后能够在res/values/values.xml
中找到以下定义:
<style name="Widget.Design.BottomNavigationView" parent="">
<item name="itemBackground">?attr/selectableItemBackgroundBorderless</item>
<item name="elevation">@dimen/design_bottom_navigation_elevation</item>
</style>复制代码
因此其实安卓帮咱们默认定义了background和elevation,咱们才能够直接看到阴影和使用colorControlHighlight
来改变ripple的颜色。关于elevation这里在说一句,源码中使用的是ViewCompat的setElevation方法设置的,但在5.0以前的版本,对应的方法是空实现的,因此才会有addCompatibilityTopDivider(context);
手动作一步兼容处理。在5.0以后的版本,记得必定要给BottomNavigationView设置背景色,不然elevation就无效了。
最后关于inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
这句,涉及的内容比较多,除了加载菜单以外,还包括了建立视图等操做,后面会再提到。
到此为止,BottomNavigationView比较主线的工做就完成了,下面再来看一下BottomNavigationMenuView。
其实它才是咱们真正看到的底栏,继承自ViewGroup,完成对底栏中每个选项视图(BottomNavigationItemView)的建立、测量、布局、更新等操做。下面给出几个全局变量的含义:
private final int mInactiveItemMaxWidth; //未选中ItemView最大宽度
private final int mInactiveItemMinWidth; //未选中ItemView的最小宽度
private final int mActiveItemMaxWidth; //选中的ItemView的最大宽度
private final int mItemHeight; //ItemView高度
private final Pools.Pool<BottomNavigationItemView> mItemPool //ItemView回收池
private boolean mShiftingMode //是否为漂移模式
private BottomNavigationItemView[] mButtons; //存储ItemView数组复制代码
不出意外的话,前四个变量是设计师给出的参数。从这几个参数中咱们能够大概推断出设计师的意图:ItemView的高度是定死的,而宽度的话会比较灵活。因为只给出了选中的ItemView的最大宽度,因此,在漂移模式的状况下,算法上应尽可能让选中的Item越大越好,但不要超过maxWidth,有些状况下(如横屏)底栏的空间会很充足,这时候也要对未选中的选项的最大宽度加以限制,避免图标间距过大。固然这也只是我的的猜想,你们权当参考。咱们直接来看一下onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//父布局宽度
final int width = MeasureSpec.getSize(widthMeasureSpec);
//Item个数
final int count = getChildCount();
//Item高度布局参数
final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
//若是为漂移模式
if (mShiftingMode) {
final int inactiveCount = count - 1;
final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
int extra = width - activeWidth - inactiveWidth * inactiveCount;
for (int i = 0; i < count; i++) {
mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
if (extra > 0) {
mTempChildWidths[i]++;
extra--;
}
}
//非漂移模式
} else {
final int maxAvailable = width / (count == 0 ? 1 : count);
final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
int extra = width - childWidth * count;
for (int i = 0; i < count; i++) {
mTempChildWidths[i] = childWidth;
if (extra > 0) {
mTempChildWidths[i]++;
extra--;
}
}
}
//调用每个子View的measure确立子布局宽高
int totalWidth = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
child.measure(MeasureSpec.makeMeasureSpec(mTempChildWidths[i], MeasureSpec.EXACTLY),
heightSpec);
ViewGroup.LayoutParams params = child.getLayoutParams();
params.width = child.getMeasuredWidth();
totalWidth += child.getMeasuredWidth();
}
//确立本身的宽高
setMeasuredDimension(
View.resolveSizeAndState(totalWidth,
MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
View.resolveSizeAndState(mItemHeight, heightSpec, 0));
}复制代码
你们能够看到,漂移模式跟非漂移模式的宽度测量方式是不一样的,经过mShiftingMode控制。有没有很疑惑mShiftingMode在什么时机被赋值的?坦率的讲,mShiftingMode必定在onMeasure以前就被赋值了,并且触发的时机就是前面没有详细解释的inflate方法。但按照咱们正常的编码思路,mShiftingMode彻底能够在onMeasure方法中根据参数count来决定,并且有很高的安全性,但Google为何没有这样作?还记得开头提到的修改样式的那篇文章吗,有没有想过凭什么简简单单的反射一个变量就能把样式改了?为何那波操做看似惊心动魄却又稳如狗?细思极恐了吧。扯远了,下面咱们来分析一下宽度计算的算法,以漂移模式为例,我把代码摘出来:
//未选中的item的数量
final int inactiveCount = count - 1;
//先根 据未选中的item 的最小宽度来计算一个 选中的item 的宽度
final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
//若是这个宽度太大,则限制为mActiveItemMaxWidth
final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
//选中的item的 宽度 确立下来以后,平分剩余宽度做为 未选中的item的 宽度
final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
//但这个宽度可能过大,限制为mInactiveItemMaxWidth
final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
//extra部分
int extra = width - activeWidth - inactiveWidth * inactiveCount;
for (int i = 0; i < count; i++) {
mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
if (extra > 0) {
mTempChildWidths[i]++;
extra--;
}
}复制代码
前几行的注释已经写的很是清楚了,至于为何会出现一个extra部分,我想是在作除法的过程当中,可能会产生精度损失,因此理想状况下,extra的值应该为零。你们能够看到,在for循环中,每一趟循环都会从extra中拿出一个像素(若是extra一直大于0的话)来弥补这个损失,至关于把不能整除的余数一个个的分给子View,送完即止。
非漂移的状况下算法更为简单,这里就再也不分析了,通过measure以后会在onLayout方法中横向排列他们,测量和布局的流程就结束了。下面来看一下buildMenuView方法:
public void buildMenuView() {
//移除全部子View
removeAllViews();
//回收移除的View
if (mButtons != null) {
for (BottomNavigationItemView item : mButtons) {
mItemPool.release(item);
}
}
if (mMenu.size() == 0) {
mSelectedItemId = 0;
mSelectedItemPosition = 0;
mButtons = null;
return;
}
mButtons = new BottomNavigationItemView[mMenu.size()];
//在这里设置ShiftingMode
mShiftingMode = mMenu.size() > 3;
for (int i = 0; i < mMenu.size(); i++) {
//挂起presenter
mPresenter.setUpdateSuspended(true);
mMenu.getItem(i).setCheckable(true);
//激活presenter
mPresenter.setUpdateSuspended(false);
//从缓冲池中获取或直接new一个BottomNavigationItemView
BottomNavigationItemView child = getNewItem();
mButtons[i] = child;
child.setIconTintList(mItemIconTint);
child.setTextColor(mItemTextColor);
child.setItemBackground(mItemBackgroundRes);
child.setShiftingMode(mShiftingMode);
//根据MenuItem的数据设置BottomNavigationItemView的显示效果
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
//添加点击监听
child.setOnClickListener(mOnClickListener);
addView(child);
}
mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
//将mSelectedItemPosition位置的MenuItem设置为选中状态,这将会引发视图更新
mMenu.getItem(mSelectedItemPosition).setChecked(true);
}复制代码
buildMenuView
的主要做用是建立多个BottomNavigationItemView并经过addView
添加为本身的子View。与之对应的一个方法是updateMenuView
,他会一次性更新全部的BottomNavigationItemView。注意,更新有多是菜单项的添加或删除引发的,因此每当出现这种状况,他的作法是把全部View都删掉,重建菜单,因而你会看到开头的第一句。但重建菜单很粗暴,会影响性能,因而这里又引入了一个回收池。这个回收池是v4包提供的一个工具类,仍是很是实用的,你们能够尝试用起来。可能你们注意到了下面这段代码:
//挂起presenter
mPresenter.setUpdateSuspended(true);
//设置为可选中的
mMenu.getItem(i).setCheckable(true);
//激活presenter
mPresenter.setUpdateSuspended(false);复制代码
这个挂起显得很是扎眼,毕竟安卓的UI操做是单线程的。并且这个所谓的挂起,只是设置一个Boolean类型的变量mUpdateSuspended
:
public void setUpdateSuspended(boolean updateSuspended) {
mUpdateSuspended = updateSuspended;
}复制代码
那这就显得很蹊跷,让咱们看一下“临界区”中都作了些什么:
@Override
public MenuItem setCheckable(boolean checkable) {
final int oldFlags = mFlags;
//根据checkable设置标志位,位与取反是清空,或操做是设置标志位
mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
if (oldFlags != mFlags) {
//看这里,传入了false
mMenu.onItemsChanged(false);
}
return this;
}复制代码
在checkable发生变化的状况下会调用到mMenu.onItemsChanged
,跟进之:
public void onItemsChanged(boolean structureChanged) {
if (!mPreventDispatchingItemsChanged) {
if (structureChanged) {
mIsVisibleItemsStale = true;
mIsActionItemsStale = true;
}
//structureChanged此时为false
dispatchPresenterUpdate(structureChanged);
} else {
mItemsChangedWhileDispatchPrevented = true;
if (structureChanged) {
mStructureChangedWhileDispatchPrevented = true;
}
}
}复制代码
在onItemsChanged
方法中会调用到dispatchPresenterUpdate
方法:
private void dispatchPresenterUpdate(boolean cleared) {
if (mPresenters.isEmpty()) return;
stopDispatchingItemsChanged();
for (WeakReference<MenuPresenter> ref : mPresenters) {
final MenuPresenter presenter = ref.get();
if (presenter == null) {
mPresenters.remove(ref);
} else {
//看这里!此时cleared为false
presenter.updateMenuView(cleared);
}
}
startDispatchingItemsChanged();
}复制代码
以后会调用到presenter.updateMenuView(cleared);
,继续跟进
@Override
public void updateMenuView(boolean cleared) {
//由于我恰好碰见你?
if (mUpdateSuspended) return;
if (cleared) {
//mMenuView就是BottomNavigationMenuView
mMenuView.buildMenuView();
} else {
//若是没有第一句的话,按照clear的值应该会走这里
mMenuView.updateMenuView();
}
}复制代码
咱们终于发现了mUpdateSuspended
的做用,你应该还没乱吧?从新梳理一下,在setCheckable()
以前,首先调用了presenter的setUpdateSuspended()
将mUpdateSuspended
置为false,以后的setCheckable()
会展转调用到presenter的updateMenuView()
,此时由于mUpdateSuspended
为false,函数直接return了,并无执行,不然可能会调用到BottomNavigationMenuView
的updateMenuView
方法,也就是那个一次性更新全部ItemView的方法,这是咱们不肯意看到的,毕竟setCheckable()
出如今一个循环之中,咱们彻底有理由让这个循环结束再统一更新他们。
再从新回过头来看这个函数的命名,就显得颇有意思了,虽然不是真的操做进程,但presenter确实不工做了,等到mUpdateSuspended
设置为true的时候再激活它。值得一说的是,有的时候挂起presenter不是为了性能,而是不挂起presenter代码就会死循环。。好比咱们在mMenuView
的updateMenuView
方法中调用setCheckable()
,就会展转调用回updateMenuView
。。具体我就很少说了,把代码写成这样也是没谁了。。
再回到buildMenuView
,其中的最后一句mMenu.getItem(mSelectedItemPosition).setChecked(true);
跟setCheckable()
有差很少的调用链,但由于没有挂起,因此会展转调用到mMenuView.updateMenuView();
,咱们仍是看一下吧:
public void updateMenuView() {
final int menuSize = mMenu.size();
if (menuSize != mButtons.length) {
// The size has changed. Rebuild menu view from scratch.
buildMenuView();
return;
}
int previousSelectedId = mSelectedItemId;
for (int i = 0; i < menuSize; i++) {
mPresenter.setUpdateSuspended(true);
MenuItem item = mMenu.getItem(i);
if (item.isChecked()) {
mSelectedItemId = item.getItemId();
mSelectedItemPosition = i;
}
//根据MenuItem更新BottomNavigationItemView
mButtons[i].initialize((MenuItemImpl) item, 0);
mPresenter.setUpdateSuspended(false);
}
if (previousSelectedId != mSelectedItemId) {
//经过TransitionManager执行动画
TransitionManager.beginDelayedTransition(this);
}
}复制代码
你可能会注意到,这里也有presenter的挂起,但我能够负责任的告诉你们,这里的挂起并有什么做用,这口锅26-alpha必须背!这彻底就是版本迭代的时候忘记删除这段代码了!不信你们能够去看25版本的源码,他确定是忘记删了!别问我花了多久才弄明白的!动画切换部分使用的是support包中的TransitionManager,支持到4.0.3,不是那个5.0的TransitionManager,因此兼容性上没有问题。可是!BottomNavigationMenuView
的全局变量中有一个private final TransitionSet mSet;
,并且还有这个
mSet = new AutoTransition();
mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
mSet.setInterpolator(new FastOutSlowInInterpolator());
mSet.addTransition(new TextScale());复制代码
而后我就找啊,这个mSet在哪用的啊?我就想把那个写源码的人叫过来,问问这个mSet在哪用的?恩?在哪?是否是在25版本里用的?心累。
还剩下最后一个,BottomNavigationItemView
,负责MenuItem的显示工做,自己是个FrameLayout,经过布局文件加载子View:sdk/extra/android/m2repository/com/android/support/design/26.0.0-alpha1/res/layout/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_gravity="bottom|center_horizontal" android:clipToPadding="false" android:paddingBottom="10dp" 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:singleLine="true" 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:singleLine="true" android:duplicateParentState="true" />
</android.support.design.internal.BaselineLayout>
</merge>复制代码
文字部分经过BaseLineLayout展现,这个Layout的做用是将子View对齐BaseLine排布在一块儿。构造函数没什好说的,咱们重点关注一下setChecked
方法:
@Override
public void setChecked(boolean checked) {
//旋转中心,用于scale
mLargeLabel.setPivotX(mLargeLabel.getWidth() / 2);
mLargeLabel.setPivotY(mLargeLabel.getBaseline());
mSmallLabel.setPivotX(mSmallLabel.getWidth() / 2);
mSmallLabel.setPivotY(mSmallLabel.getBaseline());
if (mShiftingMode) {
if (checked) {
LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
//被选中状况下将Gravity设置为TOP,由于未被选中下只是居中,因此TransitionManager会施加纵向的位移动画
iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
iconParams.topMargin = mDefaultMargin;
//此方法会引发父布局从新测量,宽度增长,从而触发横向的位移动画
mIcon.setLayoutParams(iconParams);
//无论是checked仍是unchecked,都是经过改变mLargeLabel的scale实现
mLargeLabel.setVisibility(VISIBLE);
mLargeLabel.setScaleX(1f);
mLargeLabel.setScaleY(1f);
} else {
//...
}
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、mSmallLabel的轮番显示来实现
mLargeLabel.setVisibility(VISIBLE);
mSmallLabel.setVisibility(INVISIBLE);
mLargeLabel.setScaleX(1f);
mLargeLabel.setScaleY(1f);
//虽然mSmallLabel被隐藏了,但将其放大到mLargeLabel的大小以便设置为unchecked时能够得到天然的过渡动画
mSmallLabel.setScaleX(mScaleUpFactor);
mSmallLabel.setScaleY(mScaleUpFactor);
} else {
//...
}
}
refreshDrawableState();
}复制代码
此方法根据参数checked切换视图状态。经过注释能够发现,因为TransitionManager的存在,BottomNavigationItemView并不须要处理动画过渡,仍是很是方便的。最后一句话refreshDrawableState();
使得BottomNavigationItemView也不须要亲自处理颜色切换,这才是正确的编码姿式。关于drawable状态切换,你们能够参考洋神的这篇文章。
回到文章开头跳过的inflateMenu
方法,咱们从这里入手,研究一下BottomNavigationView是如何初始化的。
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, mMenu);
mPresenter.setUpdateSuspended(false);
mPresenter.updateMenuView(true);
}复制代码
在调用inflater的inflate方法以前,presenter就挂起了,这是由于inflate方法会出触发图更新,简单的跟踪一下:
public void inflate(@MenuRes int menuRes, Menu menu) {
XmlResourceParser parser = null;
try {
//...
parseMenu(parser, attrs, menu);
} catch (XmlPullParserException e) {
//...
}
}复制代码
会调用parseMenu
解析menu文件:
private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) throws XmlPullParserException, IOException {
MenuState menuState = new MenuState(menu);
//。。。
boolean reachedEndOfMenu = false;
while (!reachedEndOfMenu) {
switch (eventType) {
case XmlPullParser.START_TAG:
//...
break;
case XmlPullParser.END_TAG:
tagName = parser.getName();
//...
else if (tagName.equals(XML_ITEM)) {
if (!menuState.hasAddedItem()) {
//if...
else {
//看这里!
registerMenu(menuState.addItem(), attrs);
}
}
}
break;
}
eventType = parser.next();
}
}复制代码
此方法会将解析出来的数据放置在menu对象中,从而完成inflate操做。看一眼menuState.addItem()
方法:
public MenuItem addItem() {
itemAdded = true;
MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);
setItem(item);
return item;
}复制代码
menu.add()
会展转调用到BottomNavigationMenu的addInternal方法,就是前面讲到的限制item个数以及设置exclusive的地方。下面的一句setItem
:
private void setItem(MenuItem item) {
item.setChecked(itemChecked)
.setXXX...
.setXXX...
...
//...
}复制代码
就会调用到setchecked了:
@Override
public MenuItem setChecked(boolean checked) {
if ((mFlags & EXCLUSIVE) != 0) {
mMenu.setExclusiveItemChecked(this);
} else {
setCheckedInt(checked);
}
return this;
}复制代码
由于咱们设置过标志位,因此执行mMenu.setExclusiveItemChecked(this)
void setExclusiveItemChecked(MenuItem item) {
final int group = item.getGroupId();
final int N = mItems.size();
stopDispatchingItemsChanged();
for (int i = 0; i < N; i++) {
MenuItemImpl curItem = mItems.get(i);
if (curItem.getGroupId() == group) {
if (!curItem.isExclusiveCheckable()) continue;
if (!curItem.isCheckable()) continue;
curItem.setCheckedInt(curItem == item);
}
}
startDispatchingItemsChanged();
}
`复制代码
注意这个for循环,对于menu中的每个item,检查其是否与参数item的引用一致,只有一致的,才会将checked设置为true,其余只能是false,因此,对于那些带有Exclusive标记的item,只能使用item.setChecked(true)
来选中它,别想着传入false进行进行反向操做,由于这与传入true的结果是同样的。在for循环中,会调用到curItem.setCheckedInt(curItem == item)
,跟进之:
void setCheckedInt(boolean checked) {
final int oldFlags = mFlags;
//根据checked设置标志位,位与取反是清空,或操做是设置标志位
mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
if (oldFlags != mFlags) {
mMenu.onItemsChanged(false);
}
}复制代码
当checked发生变化就会调用mMenu.onItemsChanged(false);
,而后就与以前提到的调用链一致了。因而在inflate操做以前,必须挂起presenter,不然将致使视图屡次更新。再回到inflate方法:
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, mMenu);
mPresenter.setUpdateSuspended(false);
mPresenter.updateMenuView(true);
}复制代码
菜单文件加载完毕,数据被存放在mMenu对象中,以后就会调用mPresenter.updateMenuView(true);
:
@Override
public void updateMenuView(boolean cleared) {
if (mUpdateSuspended) return;
if (cleared) {
mMenuView.buildMenuView();
} else {
mMenuView.updateMenuView();
}
}复制代码
而后是mMenuView.buildMenuView();
:
public void buildMenuView() {
removeAllViews()
//...
mButtons = new BottomNavigationItemView[mMenu.size()];
mShiftingMode = mMenu.size() > 3;
for (int i = 0; i < mMenu.size(); i++) {
//...
BottomNavigationItemView child = getNewItem();
mButtons[i] = child;
//...
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
child.setOnClickListener(mOnClickListener);
addView(child);
}
mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
mMenu.getItem(mSelectedItemPosition).setChecked(true);
}复制代码
到这里你们就很熟悉了,mShiftingMode也是在这里赋值的,BottomNavigationMenuView的子View也是在这个地方建立的,他们都发生在BottomNavigationView的构造函数中,最后一句mMenu.getItem(mSelectedItemPosition).setChecked(true);
将当前记录的选项设置为选中状态,由于是初始化,因此是0。后面的步骤已经讲解过了:因为setChecked,MenuItem#setChecked
=> MenuBuilder#setExclusiveItemChecked
=>MenuItem#setCheckedInt
=> MenuBuilder#onItemsChanged
=> MenuBuilder#dispatchPresenterUpdate
=>MenuPresenter#updateMenuView
=> MenuView#updateMenuView
=> BottomNavigationView#initialize
以后通过measure、layout、draw的操做,咱们就能够看到这些小可爱了...
很差意思,废话太多,文章很长,最后一部分,分析一下点击事件的传递及处理流程。在BottomNavigationMenuView的buildMenuView方法中,为每个BottomNavigationItemView设置了点击监听,onclick方法以下:
mOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
MenuItem item = itemView.getItemData();
if (!mMenu.performItemAction(item, mPresenter, 0)) {
item.setChecked(true);
}
}
};复制代码
会首先调用mMenu.performItemAction
:
public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
MenuItemImpl itemImpl = (MenuItemImpl) item;
if (itemImpl == null || !itemImpl.isEnabled()) {
return false;
}
boolean invoked = itemImpl.invoke();
//...
return invoked;
}复制代码
会进入被点击的这个item的invoke方法:
public boolean invoke() {
//...
if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) {
return true;
}
//...
return false;
}复制代码
而后又会回到MenuBuilder中去,调用他的dispatchMenuItemSelected方法:
boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
return mCallback != null && mCallback.onMenuItemSelected(menu, item);
}复制代码
咱们能够看到事件跑到callback中去了,那callback在哪呢?其实咱们在BottomNavigationView的构造函数中设置过他:
mMenu.setCallback(new MenuBuilder.Callback() {
@Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) {
mReselectedListener.onNavigationItemReselected(item);
return true; // item is already selected
}
return mSelectedListener != null
&& !mSelectedListener.onNavigationItemSelected(item);
}
@Override
public void onMenuModeChange(MenuBuilder menu) {}
});复制代码
如今事件的处理权回到了BottomNavigationView中,他的处理方式就是让咱们本身处理,也就是传递给mReselectedListener或mSelectedListener,若是咱们在外部的监听中返回了true,则callback返回false,则MenuItem的invoke方法返回false,则MenuBuilder的performItemAction返回false,则BottomNavigationItemView的点击监听中的条件判断成立:
if (!mMenu.performItemAction(item, mPresenter, 0)) {
item.setChecked(true);
}复制代码
因而会调用MenuItem的setChecked方法更新视图,不然将不作处理。
安卓的源码老是有不少值得咱们学习的地方,好比Transition的运用和Drawable状态的处理。但此次的BottomNavigationView看得我很心累,多是alpha版本的缘由,总有一种施工现场的感受...Menu框架从API level 1 就已经被设计好,经历了26个系统版本的变化,支撑着ActionBar、Toolbar、PopupMenu、NavigationView、BottomNavigationView等上层设计,基本上已经修炼成精,因此此次加入的suspend,也是无奈之举修修补补又一年吧 :)