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 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源码设计模式》

发布了161 篇原创文章 · 获赞 154 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/86994959
今日推荐