側滑菜單之NavigationView原理分析

大家好,上次我們分析了側滑菜單DrawerLayout的實現原理,明白了它是如何管理主體內容和側滑菜單之間的關系,包括布局,觸摸事件等的分析。我們同時也知道,側滑菜單的內容大致上是頂部一塊頭像內容區(qū)域,下面是一系列的菜單項,那么它的菜單內容是如何實現的呢,我們接著分析。

本次的分析內容主要為以下幾項:

  1. 結構分析
  2. 流程分析
  3. 菜單內容布局實現
  4. 菜單解析實現

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的實現思路,整體的架構設計等。實現一個功能并不算高明,更重要是如何設計,使它結構更加清晰,各個模塊層次分明,職責清晰,可擴展性更高,我覺得這應該算是編程的一個樂趣吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容