侧滑菜单之NavigationView原理分析
来源:互联网 发布:淘宝点击图片跳转链接 编辑:程序博客网 时间:2024/05/19 01:13
大家好,上次我们分析了侧滑菜单DrawerLayout的实现原理,明白了它是如何管理主体内容和侧滑菜单之间的关系,包括布局,触摸事件等的分析。我们同时也知道,侧滑菜单的内容大致上是顶部一块头像内容区域,下面是一系列的菜单项,那么它的菜单内容是如何实现的呢,我们接着分析。
本次的分析内容主要为以下几项:
- 结构分析
- 流程分析
- 菜单内容布局实现
- 菜单解析实现
1.结构分析
本次分析涉及的类有如下:
NavigationView
即是菜单内容的总体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。
NavigationMenuPresenter
实现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);}
NavigationMenu
菜单内容解析类,继承自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);}
NavigationMenuView
它才是真正的菜单内容显示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; }}
NavigationMenuAdapter
和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的实现思路,整体的架构设计等。实现一个功能并不算高明,更重要是如何设计,使它结构更加清晰,各个模块层次分明,职责清晰,可扩展性更高,我觉得这应该算是编程的一个乐趣吧。
- 侧滑菜单之NavigationView原理分析
- 侧滑菜单 Drawerlayout navigationView
- NavigationView原生侧滑菜单
- NavigationView+DrawerLayout 侧滑菜单
- Material Design之侧滑菜单DrawerLayout+NavigationView的使用
- 抽屉菜单与侧滑菜单. NavigationView
- 侧滑菜单 drawerlayout 与 NavigationView
- Iwfu-NavigationView实现侧滑菜单
- NavigationView+Drawerlayout+Toolbar实现侧滑菜单
- 更改NavigationView侧滑菜单文字颜色
- NavigationView侧滑菜单 使用教程
- DrawerLayout + NavigationView实现侧滑菜单
- DrawerLayout+NavigationView实现侧滑菜单
- DrawerLayer+NavigationView 实现侧滑菜单
- 两分钟实现 NavigationView 侧滑菜单
- NavigationView 实现侧滑菜单 改变菜单颜色的属性
- (三十一) NavigationView 原理分析
- Android DrawerLayout+NavigationView布局实现左右两边侧滑菜单
- calcHist()直方图
- 内存(RAM或ROM)和FLASH存储的真正区别总结
- centos利用yum安装卸载软件常用命令
- 【HDU
- NSDate基础用法
- 侧滑菜单之NavigationView原理分析
- vijos多边形(区间dp)dp太弱了
- 如何导入python模块?
- 【POI】Apache POI字体/Fonts(八)
- 欧几里得除法和扩展欧几里得定理
- PS 选区的基础使用
- AI创业者如何成为风口上独角兽
- 分享一个快速加载dex文件的方法
- CCPC-哈尔滨赛区:划水之旅