NavigationView实际上是一个FrameLayout,这个FrameLayout中又包含了一个RecyclerView。如果用户设置了Header布局,那么NavigationView就把这个Header作为这个RecyclerView的第一个Item View,在Header的下面就是菜单列表。通过这种封装使得构建用户菜单变得非常简单,而不需要用户每次都通过RecyclerView手动设置header和菜单,提高了工程师的开发效率。
NavigationView就是一个MVP设计模式,Toolbar的菜单解析也遵循MVP设计模式。由于Toolbar的MVP比较复杂,我们就通过剖析NavigationView的案例来学习MVP的运用。
NavigationView的构造方法:
NavigationMenuPresenter的getMenuView方法:
NavigationMenuPresenter加载菜单方法:inflateMenu
NavigationMenuPresenter加载head view的方法:addHeaderView
NavigationView中MVP的使用:
NavigationView的OnNavigationItemSelectedListener的作用:
其实NavigatioNmenuPresenter持有NavigationMenu,NavigationMenu选中会通知触发OnNavigationItemSelectedListener的onNavigationItemSelected方法,其实也类似于NavigatioNmenuPresenter在通知OnNavigationItemSelectedListener执行onNavigationItemSelected方法。
NavigationView中的Model层NavigationItem
在使用NavigationView的时候,app:menu设置菜单项;app:headerLayout设置菜单Header。
NavigationView实际上是一个FrameLayout,确切的说它继承自FrameLayout。
public class ScrimInsetsFrameLayout extends FrameLayout {
}
public class NavigationView extends ScrimInsetsFrameLayout {
//菜单Presenter
private final NavigationMenu mMenu;
private final NavigationMenuPresenter mPresenter = new NavigationMenuPresenter();
OnNavigationItemSelectedListener mListener;
private int mMaxWidth;
//菜单解析的Inflater
private MenuInflater mMenuInflater;
}
public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
// Create the menu
mMenu = new NavigationMenu(context);
//其他初始化操作
// Custom attributes
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.NavigationView, defStyleAttr,
R.style.Widget_Design_NavigationView);
ViewCompat.setBackground(
this, a.getDrawable(R.styleable.NavigationView_android_background));
if (a.hasValue(R.styleable.NavigationView_elevation)) {
ViewCompat.setElevation(this, a.getDimensionPixelSize(
R.styleable.NavigationView_elevation, 0));
}
ViewCompat.setFitsSystemWindows(this,
a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false));
mMaxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0);
final ColorStateList itemIconTint;
if (a.hasValue(R.styleable.NavigationView_itemIconTint)) {
itemIconTint = a.getColorStateList(R.styleable.NavigationView_itemIconTint);
} else {
itemIconTint = createDefaultColorStateList(android.R.attr.textColorSecondary);
}
boolean textAppearanceSet = false;
int textAppearance = 0;
if (a.hasValue(R.styleable.NavigationView_itemTextAppearance)) {
textAppearance = a.getResourceId(R.styleable.NavigationView_itemTextAppearance, 0);
textAppearanceSet = true;
}
ColorStateList itemTextColor = null;
if (a.hasValue(R.styleable.NavigationView_itemTextColor)) {
itemTextColor = a.getColorStateList(R.styleable.NavigationView_itemTextColor);
}
if (!textAppearanceSet && itemTextColor == null) {
// If there isn't a text appearance set, we'll use a default text color
itemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);
}
final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);
mMenu.setCallback(new MenuBuilder.Callback() {
@Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
return mListener != null && mListener.onNavigationItemSelected(item);
}
@Override
public void onMenuModeChange(MenuBuilder menu) {}
});
mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID);
//1.初始化一些资源
mPresenter.initForMenu(context, mMenu);
mPresenter.setItemIconTintList(itemIconTint);
if (textAppearanceSet) {
mPresenter.setItemTextAppearance(textAppearance);
}
mPresenter.setItemTextColor(itemTextColor);
mPresenter.setItemBackground(itemBackground);
mMenu.addMenuPresenter(mPresenter);
//2.构建整个菜单视图并且添加到当前视图中
addView((View) mPresenter.getMenuView(this));
//3.初始化菜单资源menu目录
if (a.hasValue(R.styleable.NavigationView_menu)) {
inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
}
//4.初始化header layout
if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
}
a.recycle();
}
#NavigationView
/**
* Inflate a menu resource into this navigation view.
*(将菜单资源扩展到此导航视图中。)
* Existing items in the menu will not be modified or removed.
*(菜单中的现有项目不会被修改或删除。)
* @param resId ID of a menu resource to inflate
*/
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, mMenu);
mPresenter.setUpdateSuspended(false);
mPresenter.updateMenuView(false);
}
#NavigationView
/**
* Inflates a View and add it as a header of the navigation menu.
*(加载视图并将其添加为导航菜单的标题。)
* @param res The layout resource ID.
* @return a newly inflated View.
*/
public View inflateHeaderView(@LayoutRes int res) {
return mPresenter.inflateHeaderView(res);
}
在NavigationView中我们可以看到熟悉的Presenter字眼,即NavigationMenuPresenter。
在NavigattionView的构造函数有几个比较重要的步骤:
1.初始化资源;
2.构建菜单和Header视图根布局;
3.解析、显示菜单项;
4.解析和显示Header视图。
在这四个步骤中,我们可以看到这四步基本上都是通过Presenter实现的,Presenter承担了几乎所有的业务逻辑。
public class NavigationMenuPresenter implements MenuPresenter {
//菜单视图,也就是一个RecyclerView
private NavigationMenuView mMenuView;
//菜单的Header布局
LinearLayout mHeaderLayout;
private Callback mCallback;
MenuBuilder mMenu;
private int mId;
NavigationMenuAdapter mAdapter;
LayoutInflater mLayoutInflater;
}
/**
* Padding for separators between items
*/
int mPaddingSeparator;
//1.初始化mLayoutInflater和MenuBuilder
@Override
public void initForMenu(Context context, MenuBuilder menu) {
mLayoutInflater = LayoutInflater.from(context);
mMenu = menu;
Resources res = context.getResources();
mPaddingSeparator = res.getDimensionPixelOffset(
R.dimen.design_navigation_separator_vertical_padding);
}
2.构建菜单和Header视图
@Override
public MenuView getMenuView(ViewGroup root) {
if (mMenuView == null) {
//加载菜单NavigationView
mMenuView = (NavigationMenuView) mLayoutInflater.inflate(
R.layout.design_navigation_menu, root, false);
if (mAdapter == null) {
mAdapter = new NavigationMenuAdapter();
}
//加载菜单的Header
mHeaderLayout = (LinearLayout) mLayoutInflater
.inflate(R.layout.design_navigation_item_header,
mMenuView, false);
mMenuView.setAdapter(mAdapter);
}
return mMenuView;
}
//3.更新菜单项
@Override
public void updateMenuView(boolean cleared) {
if (mAdapter != null) {
mAdapter.update();
}
}
//4.加载Header布局
public View inflateHeaderView(@LayoutRes int res) {
View view = mLayoutInflater.inflate(res, mHeaderLayout, false);
addHeaderView(view);
return view;
}
public void addHeaderView(@NonNull View view) {
mHeaderLayout.addView(view);
// The padding on top should be cleared.
mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom());
}
NvaigationView构造函数的第一个重要的函数是Presnter的initForMenu,在这个函数中只是进行简单的初始化操作,将mMenu对象指向构造函数传递进来的MenuBuilder,并且初始化了一些padding值。
第二个重要的函数是NavigationMenuPresenter的getMenuView函数,该函数中构造了菜单NavigationMenuView、菜单项Adapter、和Header视图。在前文中,我们提到过,NavigationMenuView本质上是一个RecyclerView,它是以数值的布局方式显示的菜单项。
public class NavigationMenuView extends RecyclerView implements MenuView {
}
public NavigationMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//布局方式是竖直线性布局
setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
}
构造完NavigationMenuView和Header视图,最后将NavigationMenuAdapter设置为NavigationMenuView的Adapter。这个NavigationMenuAdapter就负责根据视图类型来解析、绑定、展示不同非菜单项视图,比如Header视图、普通菜单项、子菜单项等。
public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
// Create the menu
mMenu = new NavigationMenu(context);
// Custom attributes
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.NavigationView, defStyleAttr,
R.style.Widget_Design_NavigationView);
ViewCompat.setBackground(
this, a.getDrawable(R.styleable.NavigationView_android_background));
if (a.hasValue(R.styleable.NavigationView_elevation)) {
ViewCompat.setElevation(this, a.getDimensionPixelSize(
R.styleable.NavigationView_elevation, 0));
}
ViewCompat.setFitsSystemWindows(this,
a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false));
mMaxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0);
final ColorStateList itemIconTint;
if (a.hasValue(R.styleable.NavigationView_itemIconTint)) {
itemIconTint = a.getColorStateList(R.styleable.NavigationView_itemIconTint);
} else {
itemIconTint = createDefaultColorStateList(android.R.attr.textColorSecondary);
}
boolean textAppearanceSet = false;
int textAppearance = 0;
if (a.hasValue(R.styleable.NavigationView_itemTextAppearance)) {
textAppearance = a.getResourceId(R.styleable.NavigationView_itemTextAppearance, 0);
textAppearanceSet = true;
}
ColorStateList itemTextColor = null;
if (a.hasValue(R.styleable.NavigationView_itemTextColor)) {
itemTextColor = a.getColorStateList(R.styleable.NavigationView_itemTextColor);
}
if (!textAppearanceSet && itemTextColor == null) {
// If there isn't a text appearance set, we'll use a default text color
itemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);
}
final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);
mMenu.setCallback(new MenuBuilder.Callback() {
@Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
return mListener != null && mListener.onNavigationItemSelected(item);
}
@Override
public void onMenuModeChange(MenuBuilder menu) {}
});
mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID);
mPresenter.initForMenu(context, mMenu);
mPresenter.setItemIconTintList(itemIconTint);
if (textAppearanceSet) {
mPresenter.setItemTextAppearance(textAppearance);
}
mPresenter.setItemTextColor(itemTextColor);
mPresenter.setItemBackground(itemBackground);
mMenu.addMenuPresenter(mPresenter);
//2.构建整个菜单视图并添加到当前视图中
addView((View) mPresenter.getMenuView(this));
//3.初始化菜单资源menu目录
if (a.hasValue(R.styleable.NavigationView_menu)) {
inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
}
//4. 初始化 header layout
if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
}
a.recycle();
}
/**
* Inflate a menu resource into this navigation view.
*(将菜单资源扩展到此导航视图中。)
* Existing items in the menu will not be modified or removed.
*
* @param resId ID of a menu resource to inflate
*/
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
//解析菜单项资源
getMenuInflater().inflate(resId, mMenu);
mPresenter.setUpdateSuspended(false);
//更新菜单视图
mPresenter.updateMenuView(false);
}
在注释3中的inflateMenu函数中,我们获取到用户设置的menu项资源,然后解析该menu资源。这个资源就是我们在前文的示例中的app:menu属性设置的menu资源,即res/menu/slide_menu.xml。里面的资源会被解析为相应的Java对象,最后添加到NavigationMenuAdapter中。
解析完菜单之后,我们再通过NavigationAdapter来更新整个菜单视图。菜单项对象会存储在mMenu对象中,然后通过Presenter的updateMenuView函数更新视图。
#NavigationMenuPresenter
@Override
public void updateMenuView(boolean cleared) {
if (mAdapter != null) {
mAdapter.update();
}
}
NavigationMenuAdapter是NavigationMenuPresenter的内部类
private class NavigationMenuAdapter extends RecyclerView.Adapter {
//视图类型,分别为菜单、子菜单、分割视图、header
private static final int VIEW_TYPE_NORMAL = 0;
private static final int VIEW_TYPE_SUBHEADER = 1;
private static final int VIEW_TYPE_SEPARATOR = 2;
private static final int VIEW_TYPE_HEADER = 3;
}
#NavigationMenuAdapter
/**
* Flattens the visible menu items of {@link #mMenu} into {@link #mItems},
* while inserting separators between items when necessary.
*/(将{@link #mMenu}的可见菜单项展平为{@link #mItems},同时在必要时在项之间插入分隔符。)
private void prepareMenuItems() {
if (mUpdateSuspended) {
return;
}
mUpdateSuspended = true;
mItems.clear();
//1.添加header视图,放在第一项
mItems.add(new NavigationMenuHeaderItem());
int currentGroupId = -1;
int currentGroupStart = 0;
boolean currentGroupHasIcon = false;
//2.从Menu中解析、添加菜单item
for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) {
MenuItemImpl item = mMenu.getVisibleItems().get(i);
if (item.isChecked()) {
setCheckedItem(item);
}
if (item.isCheckable()) {
item.setExclusiveCheckable(false);
}
if (item.hasSubMenu()) {
SubMenu subMenu = item.getSubMenu();
if (subMenu.hasVisibleItems()) {
if (i != 0) {
mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
}
//添加菜单以及子菜单
mItems.add(new NavigationMenuTextItem(item));
boolean subMenuHasIcon = false;
int subMenuStart = mItems.size();
for (int j = 0, size = subMenu.size(); j < size; j++) {
MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j);
if (subMenuItem.isVisible()) {
if (!subMenuHasIcon && subMenuItem.getIcon() != null) {
subMenuHasIcon = true;
}
if (subMenuItem.isCheckable()) {
subMenuItem.setExclusiveCheckable(false);
}
if (item.isChecked()) {
setCheckedItem(item);
}
mItems.add(new NavigationMenuTextItem(subMenuItem));
}
}
if (subMenuHasIcon) {
appendTransparentIconIfMissing(subMenuStart, mItems.size());
}
}
} else {
//添加菜单
int groupId = item.getGroupId();
if (groupId != currentGroupId) { // first item in group
currentGroupStart = mItems.size();
currentGroupHasIcon = item.getIcon() != null;
if (i != 0) {
currentGroupStart++;
mItems.add(new NavigationMenuSeparatorItem(
mPaddingSeparator, mPaddingSeparator));
}
} else if (!currentGroupHasIcon && item.getIcon() != null) {
currentGroupHasIcon = true;
appendTransparentIconIfMissing(currentGroupStart, mItems.size());
}
NavigationMenuTextItem textItem = new NavigationMenuTextItem(item);
textItem.needsEmptyIcon = currentGroupHasIcon;
mItems.add(textItem);
currentGroupId = groupId;
}
}
mUpdateSuspended = false;
}
#NavigationMenuAdapter
@Override
public int getItemViewType(int position) {
NavigationMenuItem item = mItems.get(position);
if (item instanceof NavigationMenuSeparatorItem) {
return VIEW_TYPE_SEPARATOR;
} else if (item instanceof NavigationMenuHeaderItem) {
return VIEW_TYPE_HEADER;
} else if (item instanceof NavigationMenuTextItem) {
NavigationMenuTextItem textItem = (NavigationMenuTextItem) item;
if (textItem.getMenuItem().hasSubMenu()) {
return VIEW_TYPE_SUBHEADER;
} else {
return VIEW_TYPE_NORMAL;
}
}
throw new RuntimeException("Unknown item type.");
}
#NavigationMenuAdapter
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_NORMAL:
return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
case VIEW_TYPE_SUBHEADER:
return new SubheaderViewHolder(mLayoutInflater, parent);
case VIEW_TYPE_SEPARATOR:
return new SeparatorViewHolder(mLayoutInflater, parent);
case VIEW_TYPE_HEADER:
return new HeaderViewHolder(mHeaderLayout);
}
return null;
}
#NavigationMenuAdapter
public void update() {
prepareMenuItems();
notifyDataSetChanged();
}
从代码中可以看到,在update函数中我们首先会获取到mMenu中所有的MenuItemImpl对象,MenuItemImpl就是每个菜单Java对象。然后将这些对象转换为NavigationMenuItem对象,并且添加到列表中。最后调用notifyDataSetChanged函数更新NavigationMenuView。而不同的菜单类型会有不同的ViewHoler,最终表现为不同的视觉效果。例如,header与菜单项是完全不同一样的。
至此,通过NavigationMenuView(本质为RecyclerView)构建了整个菜单视图。总结这些组件的逻辑关系:NavigationView就是MVP中的View角色,它通过Presenter处理解析、构造各种类别菜单项的业务逻辑,将自身从复杂的逻辑中解耦出来,因此NavigationView的代码非常少,而Model就是NavigationMenuItem对象,它们只是简单的实体类,负责承载菜单项的数据。
参考《Android源码设计模式》