赞
踩
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.
- *(将菜单资源扩展到此导航视图中。)
- * <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 {
-
- //菜单视图,也就是一个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.
- *(将菜单资源扩展到此导航视图中。)
- * <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
- @Override
- public void updateMenuView(boolean cleared) {
- if (mAdapter != null) {
- mAdapter.update();
- }
- }
-
- NavigationMenuAdapter是NavigationMenuPresenter的内部类
-
- private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {
- //视图类型,分别为菜单、子菜单、分割视图、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源码设计模式》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。