NavigationMenuPresenter.java revision 7c89489dcb1e7cc454da4375c387caf20201acb7
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.design.internal;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.graphics.drawable.ColorDrawable;
23import android.graphics.drawable.Drawable;
24import android.os.Bundle;
25import android.os.Parcelable;
26import android.support.annotation.LayoutRes;
27import android.support.annotation.NonNull;
28import android.support.annotation.Nullable;
29import android.support.annotation.StyleRes;
30import android.support.design.R;
31import android.support.v7.internal.view.menu.MenuBuilder;
32import android.support.v7.internal.view.menu.MenuItemImpl;
33import android.support.v7.internal.view.menu.MenuPresenter;
34import android.support.v7.internal.view.menu.MenuView;
35import android.support.v7.internal.view.menu.SubMenuBuilder;
36import android.support.v7.widget.RecyclerView;
37import android.util.SparseArray;
38import android.view.LayoutInflater;
39import android.view.MenuItem;
40import android.view.SubMenu;
41import android.view.View;
42import android.view.ViewGroup;
43import android.widget.LinearLayout;
44import android.widget.TextView;
45
46import java.util.ArrayList;
47
48/**
49 * @hide
50 */
51public class NavigationMenuPresenter implements MenuPresenter {
52
53    private static final String STATE_HIERARCHY = "android:menu:list";
54    private static final String STATE_ADAPTER = "android:menu:adapter";
55
56    private NavigationMenuView mMenuView;
57    private LinearLayout mHeaderLayout;
58
59    private Callback mCallback;
60    private MenuBuilder mMenu;
61    private int mId;
62
63    private NavigationMenuAdapter mAdapter;
64    private LayoutInflater mLayoutInflater;
65
66    private int mTextAppearance;
67    private boolean mTextAppearanceSet;
68    private ColorStateList mTextColor;
69    private ColorStateList mIconTintList;
70    private Drawable mItemBackground;
71
72    /**
73     * Padding to be inserted at the top of the list to avoid the first menu item
74     * from being placed underneath the status bar.
75     */
76    private int mPaddingTopDefault;
77
78    /**
79     * Padding for separators between items
80     */
81    private int mPaddingSeparator;
82
83    @Override
84    public void initForMenu(Context context, MenuBuilder menu) {
85        mLayoutInflater = LayoutInflater.from(context);
86        mMenu = menu;
87        Resources res = context.getResources();
88        mPaddingTopDefault = res.getDimensionPixelOffset(
89                R.dimen.design_navigation_padding_top_default);
90        mPaddingSeparator = res.getDimensionPixelOffset(
91                R.dimen.design_navigation_separator_vertical_padding);
92    }
93
94    @Override
95    public MenuView getMenuView(ViewGroup root) {
96        if (mMenuView == null) {
97            mMenuView = (NavigationMenuView) mLayoutInflater.inflate(
98                    R.layout.design_navigation_menu, root, false);
99            if (mAdapter == null) {
100                mAdapter = new NavigationMenuAdapter();
101            }
102            mHeaderLayout = (LinearLayout) mLayoutInflater
103                    .inflate(R.layout.design_navigation_item_header,
104                            mMenuView, false);
105            mMenuView.setAdapter(mAdapter);
106        }
107        return mMenuView;
108    }
109
110    @Override
111    public void updateMenuView(boolean cleared) {
112        if (mAdapter != null) {
113            mAdapter.update();
114        }
115    }
116
117    @Override
118    public void setCallback(Callback cb) {
119        mCallback = cb;
120    }
121
122    @Override
123    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
124        return false;
125    }
126
127    @Override
128    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
129        if (mCallback != null) {
130            mCallback.onCloseMenu(menu, allMenusAreClosing);
131        }
132    }
133
134    @Override
135    public boolean flagActionItems() {
136        return false;
137    }
138
139    @Override
140    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
141        return false;
142    }
143
144    @Override
145    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
146        return false;
147    }
148
149    @Override
150    public int getId() {
151        return mId;
152    }
153
154    public void setId(int id) {
155        mId = id;
156    }
157
158    @Override
159    public Parcelable onSaveInstanceState() {
160        Bundle state = new Bundle();
161        if (mMenuView != null) {
162            SparseArray<Parcelable> hierarchy = new SparseArray<>();
163            mMenuView.saveHierarchyState(hierarchy);
164            state.putSparseParcelableArray(STATE_HIERARCHY, hierarchy);
165        }
166        if (mAdapter != null) {
167            state.putBundle(STATE_ADAPTER, mAdapter.createInstanceState());
168        }
169        return state;
170    }
171
172    @Override
173    public void onRestoreInstanceState(Parcelable parcelable) {
174        Bundle state = (Bundle) parcelable;
175        SparseArray<Parcelable> hierarchy = state.getSparseParcelableArray(STATE_HIERARCHY);
176        if (hierarchy != null) {
177            mMenuView.restoreHierarchyState(hierarchy);
178        }
179        Bundle adapterState = state.getBundle(STATE_ADAPTER);
180        if (adapterState != null) {
181            mAdapter.restoreInstanceState(adapterState);
182        }
183    }
184
185    public void setCheckedItem(MenuItemImpl item) {
186        mAdapter.setCheckedItem(item);
187    }
188
189    public View inflateHeaderView(@LayoutRes int res) {
190        View view = mLayoutInflater.inflate(res, mHeaderLayout, false);
191        addHeaderView(view);
192        return view;
193    }
194
195    public void addHeaderView(@NonNull View view) {
196        mHeaderLayout.addView(view);
197        // The padding on top should be cleared.
198        mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom());
199    }
200
201    public void removeHeaderView(@NonNull View view) {
202        mHeaderLayout.removeView(view);
203        if (mHeaderLayout.getChildCount() == 0) {
204            mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());
205        }
206    }
207
208    @Nullable
209    public ColorStateList getItemTintList() {
210        return mIconTintList;
211    }
212
213    public void setItemIconTintList(@Nullable ColorStateList tint) {
214        mIconTintList = tint;
215        updateMenuView(false);
216    }
217
218    @Nullable
219    public ColorStateList getItemTextColor() {
220        return mTextColor;
221    }
222
223    public void setItemTextColor(@Nullable ColorStateList textColor) {
224        mTextColor = textColor;
225        updateMenuView(false);
226    }
227
228    public void setItemTextAppearance(@StyleRes int resId) {
229        mTextAppearance = resId;
230        mTextAppearanceSet = true;
231        updateMenuView(false);
232    }
233
234    public Drawable getItemBackground() {
235        return mItemBackground;
236    }
237
238    public void setItemBackground(Drawable itemBackground) {
239        mItemBackground = itemBackground;
240    }
241
242    public void setUpdateSuspended(boolean updateSuspended) {
243        if (mAdapter != null) {
244            mAdapter.setUpdateSuspended(updateSuspended);
245        }
246    }
247
248    private abstract static class ViewHolder extends RecyclerView.ViewHolder {
249
250        public ViewHolder(View itemView) {
251            super(itemView);
252        }
253
254    }
255
256    private static class NormalViewHolder extends ViewHolder {
257
258        public NormalViewHolder(LayoutInflater inflater, ViewGroup parent,
259                View.OnClickListener listener) {
260            super(inflater.inflate(R.layout.design_navigation_item, parent, false));
261            itemView.setOnClickListener(listener);
262        }
263
264    }
265
266    private static class SubheaderViewHolder extends ViewHolder {
267
268        public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) {
269            super(inflater.inflate(R.layout.design_navigation_item_subheader, parent, false));
270        }
271
272    }
273
274    private static class SeparatorViewHolder extends ViewHolder {
275
276        public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) {
277            super(inflater.inflate(R.layout.design_navigation_item_separator, parent, false));
278        }
279
280    }
281
282    private static class HeaderViewHolder extends ViewHolder {
283
284        public HeaderViewHolder(View itemView) {
285            super(itemView);
286        }
287
288    }
289
290    /**
291     * Handles click events for the menu items. The items has to be {@link NavigationMenuItemView}.
292     */
293    private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
294
295        @Override
296        public void onClick(View v) {
297            NavigationMenuItemView itemView = (NavigationMenuItemView) v;
298            setUpdateSuspended(true);
299            MenuItemImpl item = itemView.getItemData();
300            boolean result = mMenu.performItemAction(item, NavigationMenuPresenter.this, 0);
301            if (item != null && item.isCheckable() && result) {
302                mAdapter.setCheckedItem(item);
303            }
304            setUpdateSuspended(false);
305            updateMenuView(false);
306        }
307
308    };
309
310    private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {
311
312        private static final String STATE_CHECKED_ITEM = "android:menu:checked";
313
314        private static final String STATE_ACTION_VIEWS = "android:menu:action_views";
315        private static final int VIEW_TYPE_NORMAL = 0;
316        private static final int VIEW_TYPE_SUBHEADER = 1;
317        private static final int VIEW_TYPE_SEPARATOR = 2;
318        private static final int VIEW_TYPE_HEADER = 3;
319
320        private final ArrayList<NavigationMenuItem> mItems = new ArrayList<>();
321        private MenuItemImpl mCheckedItem;
322        private ColorDrawable mTransparentIcon;
323        private boolean mUpdateSuspended;
324
325        NavigationMenuAdapter() {
326            prepareMenuItems();
327        }
328
329        @Override
330        public long getItemId(int position) {
331            return position;
332        }
333
334        @Override
335        public int getItemCount() {
336            return mItems.size();
337        }
338
339        @Override
340        public int getItemViewType(int position) {
341            NavigationMenuItem item = mItems.get(position);
342            if (item instanceof NavigationMenuSeparatorItem) {
343                return VIEW_TYPE_SEPARATOR;
344            } else if (item instanceof NavigationMenuHeaderItem) {
345                return VIEW_TYPE_HEADER;
346            } else if (item instanceof NavigationMenuTextItem) {
347                NavigationMenuTextItem textItem = (NavigationMenuTextItem) item;
348                if (textItem.getMenuItem().hasSubMenu()) {
349                    return VIEW_TYPE_SUBHEADER;
350                } else {
351                    return VIEW_TYPE_NORMAL;
352                }
353            }
354            throw new RuntimeException("Unknown item type.");
355        }
356
357        @Override
358        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
359            switch (viewType) {
360                case VIEW_TYPE_NORMAL:
361                    return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
362                case VIEW_TYPE_SUBHEADER:
363                    return new SubheaderViewHolder(mLayoutInflater, parent);
364                case VIEW_TYPE_SEPARATOR:
365                    return new SeparatorViewHolder(mLayoutInflater, parent);
366                case VIEW_TYPE_HEADER:
367                    return new HeaderViewHolder(mHeaderLayout);
368            }
369            return null;
370        }
371
372        @Override
373        public void onBindViewHolder(ViewHolder holder, int position) {
374            switch (getItemViewType(position)) {
375                case VIEW_TYPE_NORMAL: {
376                    NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView;
377                    itemView.setIconTintList(mIconTintList);
378                    if (mTextAppearanceSet) {
379                        itemView.setTextAppearance(itemView.getContext(), mTextAppearance);
380                    }
381                    if (mTextColor != null) {
382                        itemView.setTextColor(mTextColor);
383                    }
384                    itemView.setBackgroundDrawable(mItemBackground != null ?
385                            mItemBackground.getConstantState().newDrawable() : null);
386                    NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
387                    itemView.initialize(item.getMenuItem(), 0);
388                    break;
389                }
390                case VIEW_TYPE_SUBHEADER: {
391                    TextView subHeader = (TextView) holder.itemView;
392                    NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
393                    subHeader.setText(item.getMenuItem().getTitle());
394                    break;
395                }
396                case VIEW_TYPE_SEPARATOR: {
397                    NavigationMenuSeparatorItem item =
398                            (NavigationMenuSeparatorItem) mItems.get(position);
399                    holder.itemView.setPadding(0, item.getPaddingTop(), 0,
400                            item.getPaddingBottom());
401                    break;
402                }
403                case VIEW_TYPE_HEADER: {
404                    break;
405                }
406            }
407
408        }
409
410        @Override
411        public void onViewRecycled(ViewHolder holder) {
412            if (holder instanceof NormalViewHolder) {
413                ((NavigationMenuItemView) holder.itemView).recycle();
414            }
415        }
416
417        public void update() {
418            prepareMenuItems();
419            notifyDataSetChanged();
420        }
421
422        /**
423         * Flattens the visible menu items of {@link #mMenu} into {@link #mItems},
424         * while inserting separators between items when necessary.
425         */
426        private void prepareMenuItems() {
427            if (mUpdateSuspended) {
428                return;
429            }
430            mUpdateSuspended = true;
431            mItems.clear();
432            mItems.add(new NavigationMenuHeaderItem());
433
434            int currentGroupId = -1;
435            int currentGroupStart = 0;
436            boolean currentGroupHasIcon = false;
437            for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) {
438                MenuItemImpl item = mMenu.getVisibleItems().get(i);
439                if (item.isChecked()) {
440                    setCheckedItem(item);
441                }
442                if (item.isCheckable()) {
443                    item.setExclusiveCheckable(false);
444                }
445                if (item.hasSubMenu()) {
446                    SubMenu subMenu = item.getSubMenu();
447                    if (subMenu.hasVisibleItems()) {
448                        if (i != 0) {
449                            mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
450                        }
451                        mItems.add(new NavigationMenuTextItem(item));
452                        boolean subMenuHasIcon = false;
453                        int subMenuStart = mItems.size();
454                        for (int j = 0, size = subMenu.size(); j < size; j++) {
455                            MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j);
456                            if (subMenuItem.isVisible()) {
457                                if (!subMenuHasIcon && subMenuItem.getIcon() != null) {
458                                    subMenuHasIcon = true;
459                                }
460                                if (subMenuItem.isCheckable()) {
461                                    subMenuItem.setExclusiveCheckable(false);
462                                }
463                                if (item.isChecked()) {
464                                    setCheckedItem(item);
465                                }
466                                mItems.add(new NavigationMenuTextItem(subMenuItem));
467                            }
468                        }
469                        if (subMenuHasIcon) {
470                            appendTransparentIconIfMissing(subMenuStart, mItems.size());
471                        }
472                    }
473                } else {
474                    int groupId = item.getGroupId();
475                    if (groupId != currentGroupId) { // first item in group
476                        currentGroupStart = mItems.size();
477                        currentGroupHasIcon = item.getIcon() != null;
478                        if (i != 0) {
479                            currentGroupStart++;
480                            mItems.add(new NavigationMenuSeparatorItem(
481                                    mPaddingSeparator, mPaddingSeparator));
482                        }
483                    } else if (!currentGroupHasIcon && item.getIcon() != null) {
484                        currentGroupHasIcon = true;
485                        appendTransparentIconIfMissing(currentGroupStart, mItems.size());
486                    }
487                    if (currentGroupHasIcon && item.getIcon() == null) {
488                        item.setIcon(android.R.color.transparent);
489                    }
490                    mItems.add(new NavigationMenuTextItem(item));
491                    currentGroupId = groupId;
492                }
493            }
494            mUpdateSuspended = false;
495        }
496
497        private void appendTransparentIconIfMissing(int startIndex, int endIndex) {
498            for (int i = startIndex; i < endIndex; i++) {
499                NavigationMenuTextItem textItem = (NavigationMenuTextItem) mItems.get(i);
500                MenuItem item = textItem.getMenuItem();
501                if (item.getIcon() == null) {
502                    if (mTransparentIcon == null) {
503                        mTransparentIcon = new ColorDrawable(android.R.color.transparent);
504                    }
505                    item.setIcon(mTransparentIcon);
506                }
507            }
508        }
509
510        public void setCheckedItem(MenuItemImpl checkedItem) {
511            if (mCheckedItem == checkedItem || !checkedItem.isCheckable()) {
512                return;
513            }
514            if (mCheckedItem != null) {
515                mCheckedItem.setChecked(false);
516            }
517            mCheckedItem = checkedItem;
518            checkedItem.setChecked(true);
519        }
520
521        public Bundle createInstanceState() {
522            Bundle state = new Bundle();
523            if (mCheckedItem != null) {
524                state.putInt(STATE_CHECKED_ITEM, mCheckedItem.getItemId());
525            }
526            // Store the states of the action views.
527            SparseArray<ParcelableSparseArray> actionViewStates = new SparseArray<>();
528            for (NavigationMenuItem navigationMenuItem : mItems) {
529                if (navigationMenuItem instanceof NavigationMenuTextItem) {
530                    MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
531                    View actionView = item != null ? item.getActionView() : null;
532                    if (actionView != null) {
533                        ParcelableSparseArray container = new ParcelableSparseArray();
534                        actionView.saveHierarchyState(container);
535                        actionViewStates.put(item.getItemId(), container);
536                    }
537                }
538            }
539            state.putSparseParcelableArray(STATE_ACTION_VIEWS, actionViewStates);
540            return state;
541        }
542
543        public void restoreInstanceState(Bundle state) {
544            int checkedItem = state.getInt(STATE_CHECKED_ITEM, 0);
545            if (checkedItem != 0) {
546                mUpdateSuspended = true;
547                for (NavigationMenuItem item : mItems) {
548                    if (item instanceof NavigationMenuTextItem) {
549                        MenuItemImpl menuItem = ((NavigationMenuTextItem) item).getMenuItem();
550                        if (menuItem != null && menuItem.getItemId() == checkedItem) {
551                            setCheckedItem(menuItem);
552                            break;
553                        }
554                    }
555                }
556                mUpdateSuspended = false;
557                prepareMenuItems();
558            }
559            // Restore the states of the action views.
560            SparseArray<ParcelableSparseArray> actionViewStates = state
561                    .getSparseParcelableArray(STATE_ACTION_VIEWS);
562            for (NavigationMenuItem navigationMenuItem : mItems) {
563                if (navigationMenuItem instanceof NavigationMenuTextItem) {
564                    MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
565                    View actionView = item != null ? item.getActionView() : null;
566                    if (actionView != null) {
567                        actionView.restoreHierarchyState(actionViewStates.get(item.getItemId()));
568                    }
569                }
570            }
571        }
572
573        public void setUpdateSuspended(boolean updateSuspended) {
574            mUpdateSuspended = updateSuspended;
575        }
576
577    }
578
579    /**
580     * Unified data model for all sorts of navigation menu items.
581     */
582    private interface NavigationMenuItem {
583    }
584
585    /**
586     * Normal or subheader items.
587     */
588    private static class NavigationMenuTextItem implements NavigationMenuItem {
589
590        private final MenuItemImpl mMenuItem;
591
592        private NavigationMenuTextItem(MenuItemImpl item) {
593            mMenuItem = item;
594        }
595
596        public MenuItemImpl getMenuItem() {
597            return mMenuItem;
598        }
599
600    }
601
602    /**
603     * Separator items.
604     */
605    private static class NavigationMenuSeparatorItem implements NavigationMenuItem {
606
607        private final int mPaddingTop;
608
609        private final int mPaddingBottom;
610
611        public NavigationMenuSeparatorItem(int paddingTop, int paddingBottom) {
612            mPaddingTop = paddingTop;
613            mPaddingBottom = paddingBottom;
614        }
615
616        public int getPaddingTop() {
617            return mPaddingTop;
618        }
619
620        public int getPaddingBottom() {
621            return mPaddingBottom;
622        }
623
624    }
625
626    /**
627     * Header (not subheader) items.
628     */
629    private static class NavigationMenuHeaderItem implements NavigationMenuItem {
630        // The actual content is hold by NavigationMenuPresenter#mHeaderLayout.
631    }
632
633}
634