androidNavigationView解析

时间: 2023-10-04 admin IT培训

android  NavigationView解析

android NavigationView解析

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 menumMenu = new NavigationMenu(context);
//其他初始化操作// Custom attributesTintTypedArray 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 coloritemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);}final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);mMenu.setCallback(new MenuBuilder.Callback() {@Overridepublic boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {return mListener != null && mListener.onNavigationItemSelected(item);}@Overridepublic 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 layoutif (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.*(将菜单资源扩展到此导航视图中。)* <p>Existing items in the menu will not be modified or removed.</p>*(菜单中的现有项目不会被修改或删除。)* @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 {//菜单视图,也就是一个RecyclerViewprivate 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@Overridepublic 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视图@Overridepublic MenuView getMenuView(ViewGroup root) {if (mMenuView == null) {//加载菜单NavigationViewmMenuView = (NavigationMenuView) mLayoutInflater.inflate(R.layout.design_navigation_menu, root, false);if (mAdapter == null) {mAdapter = new NavigationMenuAdapter();}
//加载菜单的HeadermHeaderLayout = (LinearLayout) mLayoutInflater.inflate(R.layout.design_navigation_item_header,mMenuView, false);mMenuView.setAdapter(mAdapter);}return mMenuView;}

 

 

//3.更新菜单项
@Overridepublic 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 menumMenu = new NavigationMenu(context);// Custom attributesTintTypedArray 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 coloritemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);}final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);mMenu.setCallback(new MenuBuilder.Callback() {@Overridepublic boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {return mListener != null && mListener.onNavigationItemSelected(item);}@Overridepublic 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 layoutif (a.hasValue(R.styleable.NavigationView_headerLayout)) {inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));}a.recycle();}/*** Inflate a menu resource into this navigation view.*(将菜单资源扩展到此导航视图中。)* <p>Existing items in the menu will not be modified or removed.</p>** @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@Overridepublic void updateMenuView(boolean cleared) {if (mAdapter != null) {mAdapter.update();}}NavigationMenuAdapter是NavigationMenuPresenter的内部类private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {//视图类型,分别为菜单、子菜单、分割视图、headerprivate 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中解析、添加菜单itemfor (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 groupcurrentGroupStart = 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@Overridepublic 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@Overridepublic 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;}#NavigationMenuAdapterpublic void update() {prepareMenuItems();notifyDataSetChanged();}

 

 

从代码中可以看到,在update函数中我们首先会获取到mMenu中所有的MenuItemImpl对象,MenuItemImpl就是每个菜单Java对象。然后将这些对象转换为NavigationMenuItem对象,并且添加到列表中。最后调用notifyDataSetChanged函数更新NavigationMenuView。而不同的菜单类型会有不同的ViewHoler,最终表现为不同的视觉效果。例如,header与菜单项是完全不同一样的。

 

至此,通过NavigationMenuView(本质为RecyclerView)构建了整个菜单视图。总结这些组件的逻辑关系:NavigationView就是MVP中的View角色,它通过Presenter处理解析、构造各种类别菜单项的业务逻辑,将自身从复杂的逻辑中解耦出来,因此NavigationView的代码非常少,而Model就是NavigationMenuItem对象,它们只是简单的实体类,负责承载菜单项的数据。

 

参考《Android源码设计模式》