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