大家好,上次我們分析了側滑菜單DrawerLayout的實現原理,明白了它是如何管理主體內容和側滑菜單之間的關系,包括布局,觸摸事件等的分析。我們同時也知道,側滑菜單的內容大致上是頂部一塊頭像內容區(qū)域,下面是一系列的菜單項,那么它的菜單內容是如何實現的呢,我們接著分析。
本次的分析內容主要為以下幾項:
- 結構分析
- 流程分析
- 菜單內容布局實現
- 菜單解析實現
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的優(yōu)點想必是人盡皆知吧。那么我們先猜想一下,包括頭布局,菜單項,子菜單項,分割線等實現都是通過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. 流程分析
接下來我們從入口分析主要的執(zhí)行流程,以讓我們對它的實現原理有個整體的認識。這里我會剔除一些細節(jié)和分支,專注于主要流程的執(zhí)行。
從NavigationView構造方法開始
public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
// 創(chuàng)建NavigationMenu
mMenu = new NavigationMenu(context);
// 讀取NavigationView布局中定義的屬性值,將這些屬性值交給NavigationMenuPresenter做后續(xù)的使用
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) {
//分隔區(qū)域類型
return VIEW_TYPE_SEPARATOR;
} else if (item instanceof NavigationMenuHeaderItem) {
//頭部區(qū)域類型
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: {
//分隔區(qū)域項
NavigationMenuSeparatorItem item =
(NavigationMenuSeparatorItem) mItems.get(position);
holder.itemView.setPadding(0, item.getPaddingTop(), 0,
item.getPaddingBottom());
break;
}
case VIEW_TYPE_HEADER: {
//頭部區(qū)域,它和定義的菜單數據是獨立分開的,這里不實現
break;
}
}
}
}
看到這里,我們就了解菜單項的布局了,那么頭部區(qū)域是如何處理的呢?我們繼續(xù)來看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 {
//菜單狀態(tài)類,通過它讀取,并臨時保存數據
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相關的數據,便于下次循環(huán)使用
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中了。接著繼續(xù)看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的實現思路,整體的架構設計等。實現一個功能并不算高明,更重要是如何設計,使它結構更加清晰,各個模塊層次分明,職責清晰,可擴展性更高,我覺得這應該算是編程的一個樂趣吧。