大家好,上次我们分析了侧滑菜单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的实现思路,整体的架构设计等。实现一个功能并不算高明,更重要是如何设计,使它结构更加清晰,各个模块层次分明,职责清晰,可扩展性更高,我觉得这应该算是编程的一个乐趣吧。