侧滑菜单之NavigationView原理分析

来源:互联网 发布:淘宝点击图片跳转链接 编辑:程序博客网 时间:2024/05/19 01:13

大家好,上次我们分析了侧滑菜单DrawerLayout的实现原理,明白了它是如何管理主体内容和侧滑菜单之间的关系,包括布局,触摸事件等的分析。我们同时也知道,侧滑菜单的内容大致上是顶部一块头像内容区域,下面是一系列的菜单项,那么它的菜单内容是如何实现的呢,我们接着分析。

本次的分析内容主要为以下几项:

  1. 结构分析
  2. 流程分析
  3. 菜单内容布局实现
  4. 菜单解析实现

1.结构分析

本次分析涉及的类有如下:

即是菜单内容的总体View,是所有菜单内容显示管理的一个封装,使用它有多简单,内容的提供只需要一个xml布局定义就够了。

<android.support.design.widget.NavigationView      android:id="@+id/nav_view"      android:layout_width="wrap_content"      android:layout_height="match_parent"      android:layout_gravity="start"      android:fitsSystemWindows="true"      app:headerLayout="@layout/nav_header_main2"      app:menu="@menu/activity_main2_drawer"      />

可以看到,通过指定headerLayout属性即可设置菜单的头部布局,通过指menu属性即可设置菜单的菜单项布局,当然layout_gravity同时也是需要指定的,这样DrawerLayout才能识别它为侧滑菜单View。

实现MenuPresenter接口,是实际管理菜单内容布局的负责人,是NavigationView的管家,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);}

菜单内容解析类,继承自MenuBuilder,它的工作就是负责解析上面NavigationView布局的menu属性指定的menu菜单的内容。

/** * 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);    //这里mMenu就是NavigationMenu对象,配合MenuInflater完成解析    getMenuInflater().inflate(resId, mMenu);    mPresenter.setUpdateSuspended(false);    mPresenter.updateMenuView(false);}

它才是真正的菜单内容显示View,NavigationView只是容器而已,NavigationMenuView继承RecyclerView,实现MenuView接口,看到这,是不是有点明白菜单内容布局的实现了?是的,菜单内容布局上的所有内容就是用一个RecyclerView列表实现的。包括头部的headView,还是后面才一系列菜单项View,为什么要这样实现,RecyclerView的优点想必是人尽皆知吧。那么我们先猜想一下,包括头布局,菜单项,子菜单项,分割线等实现都是通过itemViewType分别实现的。

public class NavigationMenuView extends RecyclerView implements MenuView {    public NavigationMenuView(Context context) {        this(context, null);    }    public NavigationMenuView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public NavigationMenuView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));    }    @Override    public void initialize(MenuBuilder menu) {    }    @Override    public int getWindowAnimations() {        return 0;    }}

和NavigationMenuView这个列表配对的RecyclerView适配器,用于管理和填充菜单列表数据到列表中。

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {    ...}

多种ViewHolder实现

既然有不同类型的布局,就会对应有不同的ViewHolder实现

//普通列表项private static class NormalViewHolder extends ViewHolder {    public NormalViewHolder(LayoutInflater inflater, ViewGroup parent,            View.OnClickListener listener) {        super(inflater.inflate(R.layout.design_navigation_item, parent, false));        itemView.setOnClickListener(listener);    }}//子菜单项private static class SubheaderViewHolder extends ViewHolder {    public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) {        super(inflater.inflate(R.layout.design_navigation_item_subheader, parent, false));    }}//分隔线项private static class SeparatorViewHolder extends ViewHolder {    public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) {        super(inflater.inflate(R.layout.design_navigation_item_separator, parent, false));    }}//头部项private static class HeaderViewHolder extends ViewHolder {    public HeaderViewHolder(View itemView) {        super(itemView);    }}

多种NavigationMenuItem实现

不同类型的布局,同时对应有不同的NavigationMenuItem接口实现,它是作为数据项接口,通过它获取菜单的内容数据。

/** * 普通菜单项,或者子菜单数据项 */private static class NavigationMenuTextItem implements NavigationMenuItem {    ...}/** * 分隔线数据项 */private static class NavigationMenuSeparatorItem implements NavigationMenuItem{    ...}/** * 头部数据项 */private static class NavigationMenuHeaderItem implements NavigationMenuItem {    ...}

2. 流程分析

接下来我们从入口分析主要的执行流程,以让我们对它的实现原理有个整体的认识。这里我会剔除一些细节和分支,专注于主要流程的执行。

从NavigationView构造方法开始

public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    ThemeUtils.checkAppCompatTheme(context);    // 创建NavigationMenu    mMenu = new NavigationMenu(context);    // 读取NavigationView布局中定义的属性值,将这些属性值交给NavigationMenuPresenter做后续的使用    TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,            R.styleable.NavigationView, defStyleAttr,            R.style.Widget_Design_NavigationView);    ...    //将NavigationMenu和NavigationMenuPresenter进行绑定    mMenu.addMenuPresenter(mPresenter);    //将mPresenter管理的RecyclerView布局添加到NavigationView上,所以说NavigationView只是一个容器而已    addView((View) mPresenter.getMenuView(this));    //解析菜单数据,并刷新列表显示这些菜单    if (a.hasValue(R.styleable.NavigationView_menu)) {        inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));    }    //解析头部布局    if (a.hasValue(R.styleable.NavigationView_headerLayout)) {        inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));    }    ...}

接着我们分析inflateMenu方法,解析并显示菜单数据的操作这里开始。

/** * 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);    //这里通过MenuInflater将菜单数据解析保存到NavigationMenu中    getMenuInflater().inflate(resId, mMenu);    mPresenter.setUpdateSuspended(false);    //刷新列表,更新并显示菜单    mPresenter.updateMenuView(false);}

可以看到,这里做了菜单内容的解析,然后刷新列表,显示菜单内容了。

既然我们知道菜单是由列表实现的,那我们就具体看看它是如何实现的。

2. 菜单内容布局实现

我们直接看NavigationMenuAdapter这个列表适配器

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {    NavigationMenuAdapter() {        //这里去获取所有的菜单信息        prepareMenuItems();    }    //这里去获取所有的菜单信息    private void prepareMenuItems() {        if (mUpdateSuspended) {            return;        }        mUpdateSuspended = true;        //清除之前的数据        mItems.clear();        //这里添加用于显示头部的菜单项信息,最先显示头部        mItems.add(new NavigationMenuHeaderItem());        int currentGroupId = -1;        int currentGroupStart = 0;        boolean currentGroupHasIcon = false;        //遍历所有可见的菜单项,分别处理添加到列表中        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;    }}

在列表适配器初始化时,调用prepareMenuItems准备了最终需要显示菜单项数据。有了数据之后,我们再看看其他

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {    @Override    public long getItemId(int position) {        return position;    }    @Override    public int getItemCount() {        return mItems.size();    }    @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.");    }    @Override    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        //根据不同的布局类型,返回不同的ViewHolder        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;    }    @Override    public void onBindViewHolder(ViewHolder holder, int position) {        //具体菜单项类型的内容填充了        switch (getItemViewType(position)) {            case VIEW_TYPE_NORMAL: {                //普通菜单项                NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView;                itemView.setIconTintList(mIconTintList);                if (mTextAppearanceSet) {                    itemView.setTextAppearance(mTextAppearance);                }                if (mTextColor != null) {                    itemView.setTextColor(mTextColor);                }                ViewCompat.setBackground(itemView, mItemBackground != null ?                        mItemBackground.getConstantState().newDrawable() : null);                NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);                itemView.setNeedsEmptyIcon(item.needsEmptyIcon);                itemView.initialize(item.getMenuItem(), 0);                break;            }            case VIEW_TYPE_SUBHEADER: {                //子菜单项                TextView subHeader = (TextView) holder.itemView;                NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);                subHeader.setText(item.getMenuItem().getTitle());                break;            }            case VIEW_TYPE_SEPARATOR: {                //分隔区域项                NavigationMenuSeparatorItem item =                        (NavigationMenuSeparatorItem) mItems.get(position);                holder.itemView.setPadding(0, item.getPaddingTop(), 0,                        item.getPaddingBottom());                break;            }            case VIEW_TYPE_HEADER: {                //头部区域,它和定义的菜单数据是独立分开的,这里不实现                break;            }        }    }}

看到这里,我们就了解菜单项的布局了,那么头部区域是如何处理的呢?我们继续来看NavigationMenuPresenter

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {    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());    }    public void removeHeaderView(@NonNull View view) {        //这里移除头部布局        mHeaderLayout.removeView(view);        if (mHeaderLayout.getChildCount() == 0) {            mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());        }    }    public int getHeaderCount() {        return mHeaderLayout.getChildCount();    }    public View getHeaderView(int index) {        return mHeaderLayout.getChildAt(index);    }}

我们看到上面有添加头部布局,而mHeaderLayout是包装在HeaderViewHolder中的,这样头部布局也就能显示在列表中了,而且是在第一位。接下来我们分析一个菜单xml文件定义的数据是如何解析成菜单数据的。

3. 菜单解析实现

菜单xml文件定义的数据解析成菜单数据,我们很自然的能想到,使用xml解析方式,例如android提供的PullParser,可以实现数据的解析,然后根据数据类型转换为我们需要的数据就可以了。包括布局xml文件的解析成View也是一样的道理。那么我们看看具体的实现吧。

我们这里主要分析MenuInflater这个菜单解析类。先从inflate方法开始

public class MenuInflater {    //解析菜单的入口    public void inflate(@MenuRes int menuRes, Menu menu) {        XmlResourceParser parser = null;        try {            //获取菜单资源解析器            parser = mContext.getResources().getLayout(menuRes);            AttributeSet attrs = Xml.asAttributeSet(parser);            //开始解析            parseMenu(parser, attrs, menu);        } catch (XmlPullParserException e) {            throw new InflateException("Error inflating menu XML", e);        } catch (IOException e) {            throw new InflateException("Error inflating menu XML", e);        } finally {            if (parser != null) parser.close();        }    }}

接着进入到parseMenu开始解析工作。

public class MenuInflater {    private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)            throws XmlPullParserException, IOException {        //菜单状态类,通过它读取,并临时保存数据        MenuState menuState = new MenuState(menu);        int eventType = parser.getEventType();        String tagName;        boolean lookingForEndOfUnknownTag = false;        String unknownTagName = null;        // 这里确保包含menu标签,并且menu标签在最开始,不然抛异常        do {            if (eventType == XmlPullParser.START_TAG) {                tagName = parser.getName();                if (tagName.equals(XML_MENU)) {                    // Go to next tag                    eventType = parser.next();                    break;                }                throw new RuntimeException("Expecting menu, got " + tagName);            }            eventType = parser.next();        } while (eventType != XmlPullParser.END_DOCUMENT);        //然后开始遍历处理menu的子标签        boolean reachedEndOfMenu = false;        while (!reachedEndOfMenu) {            switch (eventType) {                case XmlPullParser.START_TAG:                    if (lookingForEndOfUnknownTag) {                        break;                    }                    tagName = parser.getName();                    if (tagName.equals(XML_GROUP)) {                        //这里读取group标签                        menuState.readGroup(attrs);                    } else if (tagName.equals(XML_ITEM)) {                        //这里读取item标签                        menuState.readItem(attrs);                    } else if (tagName.equals(XML_MENU)) {                        // 这里表面遇到了子菜单标签,递归parseMenu读取子菜单数据                        SubMenu subMenu = menuState.addSubMenuItem();                        registerMenu(subMenu, attrs);                        // Parse the submenu into returned SubMenu                        parseMenu(parser, attrs, subMenu);                    } else {                        lookingForEndOfUnknownTag = true;                        unknownTagName = tagName;                    }                    break;                case XmlPullParser.END_TAG:                    //表示当前标签读取结束                    tagName = parser.getName();                    if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {                        lookingForEndOfUnknownTag = false;                        unknownTagName = null;                    } else if (tagName.equals(XML_GROUP)) {                        //读取到group的结束标签                        //重置group相关的数据,便于下次循环使用                        menuState.resetGroup();                    } else if (tagName.equals(XML_ITEM)) {                        //读取到item的结束标签                        // Add the item if it hasn't been added (if the item was                        // a submenu, it would have been added already)                        if (!menuState.hasAddedItem()) {                            if (menuState.itemActionProvider != null &&                                    menuState.itemActionProvider.hasSubMenu()) {                               //这里根据解析的数据,添加新建的子菜单item到menu中 registerMenu(menuState.addSubMenuItem(), attrs);                            } else {                                //这里根据解析的数据,添加新建的ca菜单项Item到menu中                                registerMenu(menuState.addItem(), attrs);                            }                        }                    } else if (tagName.equals(XML_MENU)) {                        //读到menu结束标签了,结束读取                        reachedEndOfMenu = true;                    }                    break;                case XmlPullParser.END_DOCUMENT:                    //表面最后没有读取到menu结束标签,menu资源错误                    throw new RuntimeException("Unexpected end of document");            }            eventType = parser.next();        }    }}

可见这里面就完成了菜单资源数据的解析,并将数据添加到menu中了。接着继续看MenuState是如何读取单个标签数据的。

private class MenuState {    //读取group标签中的设置的属性值    public void readGroup(AttributeSet attrs) {        TypedArray a = mContext.obtainStyledAttributes(attrs,                com.android.internal.R.styleable.MenuGroup);        groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);        groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);        groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);        groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);        groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);        groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);        a.recycle();    }    //读取item标签中的设置的属性值    public void readItem(AttributeSet attrs) {        TypedArray a = mContext.obtainStyledAttributes(attrs,                com.android.internal.R.styleable.MenuItem);        // Inherit attributes from the group as default value        itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);        final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);        ...        a.recycle();        itemAdded = false;    }}

接下来看MenuState是如何添加单个标签数据解析后的item的。

private class MenuState {    //添加普通菜单项item    public MenuItem addItem() {        itemAdded = true;        MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);        setItem(item);        return item;    }    //添加子菜单item    public SubMenu addSubMenuItem() {        itemAdded = true;        SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);        setItem(subMenu.getItem());        return subMenu;    }    //设置item项的数据,将MenuState当前读取到的属性值填充到该item中    private void setItem(MenuItem item) {        item.setChecked(itemChecked)            .setVisible(itemVisible)            .setEnabled(itemEnabled)            .setCheckable(itemCheckable >= 1)            .setTitleCondensed(itemTitleCondensed)            .setIcon(itemIconResId)            .setAlphabeticShortcut(itemAlphabeticShortcut)            .setNumericShortcut(itemNumericShortcut);        ...    }}

到这里的话,我们就清楚菜单资源的解析过程了。

这一篇解析中,我们清楚了侧滑菜单内部菜单的布局实现原理,通过在布局文件中给NavigationView设置headerLayout和menu就能快速实现头部布局,和菜单布局,很大的降低了耦合度,且简单清晰。结合上一篇侧滑菜单DrawerLayout的实现原理,我相信大家会对侧滑菜单有一个清楚的认识,包括自定义View的实现思路,整体的架构设计等。实现一个功能并不算高明,更重要是如何设计,使它结构更加清晰,各个模块层次分明,职责清晰,可扩展性更高,我觉得这应该算是编程的一个乐趣吧。

原创粉丝点击