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.widget;
18 
19 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.content.Context;
22 import android.content.res.ColorStateList;
23 import android.graphics.drawable.Drawable;
24 import android.os.Bundle;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.support.annotation.DrawableRes;
28 import android.support.annotation.IdRes;
29 import android.support.annotation.LayoutRes;
30 import android.support.annotation.NonNull;
31 import android.support.annotation.Nullable;
32 import android.support.annotation.RestrictTo;
33 import android.support.annotation.StyleRes;
34 import android.support.design.R;
35 import android.support.design.internal.NavigationMenu;
36 import android.support.design.internal.NavigationMenuPresenter;
37 import android.support.design.internal.ScrimInsetsFrameLayout;
38 import android.support.v4.content.ContextCompat;
39 import android.support.v4.view.AbsSavedState;
40 import android.support.v4.view.ViewCompat;
41 import android.support.v4.view.WindowInsetsCompat;
42 import android.support.v7.content.res.AppCompatResources;
43 import android.support.v7.view.SupportMenuInflater;
44 import android.support.v7.view.menu.MenuBuilder;
45 import android.support.v7.view.menu.MenuItemImpl;
46 import android.support.v7.widget.TintTypedArray;
47 import android.util.AttributeSet;
48 import android.util.TypedValue;
49 import android.view.Menu;
50 import android.view.MenuInflater;
51 import android.view.MenuItem;
52 import android.view.View;
53 
54 /**
55  * Represents a standard navigation menu for application. The menu contents can be populated
56  * by a menu resource file.
57  * <p>NavigationView is typically placed inside a {@link android.support.v4.widget.DrawerLayout}.
58  * </p>
59  * <pre>
60  * &lt;android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
61  *     xmlns:app="http://schemas.android.com/apk/res-auto"
62  *     android:id="@+id/drawer_layout"
63  *     android:layout_width="match_parent"
64  *     android:layout_height="match_parent"
65  *     android:fitsSystemWindows="true"&gt;
66  *
67  *     &lt;!-- Your contents --&gt;
68  *
69  *     &lt;android.support.design.widget.NavigationView
70  *         android:id="@+id/navigation"
71  *         android:layout_width="wrap_content"
72  *         android:layout_height="match_parent"
73  *         android:layout_gravity="start"
74  *         app:menu="@menu/my_navigation_items" /&gt;
75  * &lt;/android.support.v4.widget.DrawerLayout&gt;
76  * </pre>
77  */
78 public class NavigationView extends ScrimInsetsFrameLayout {
79 
80     private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
81     private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
82 
83     private static final int PRESENTER_NAVIGATION_VIEW_ID = 1;
84 
85     private final NavigationMenu mMenu;
86     private final NavigationMenuPresenter mPresenter = new NavigationMenuPresenter();
87 
88     OnNavigationItemSelectedListener mListener;
89     private int mMaxWidth;
90 
91     private MenuInflater mMenuInflater;
92 
NavigationView(Context context)93     public NavigationView(Context context) {
94         this(context, null);
95     }
96 
NavigationView(Context context, AttributeSet attrs)97     public NavigationView(Context context, AttributeSet attrs) {
98         this(context, attrs, 0);
99     }
100 
NavigationView(Context context, AttributeSet attrs, int defStyleAttr)101     public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
102         super(context, attrs, defStyleAttr);
103 
104         ThemeUtils.checkAppCompatTheme(context);
105 
106         // Create the menu
107         mMenu = new NavigationMenu(context);
108 
109         // Custom attributes
110         TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
111                 R.styleable.NavigationView, defStyleAttr,
112                 R.style.Widget_Design_NavigationView);
113 
114         ViewCompat.setBackground(
115                 this, a.getDrawable(R.styleable.NavigationView_android_background));
116         if (a.hasValue(R.styleable.NavigationView_elevation)) {
117             ViewCompat.setElevation(this, a.getDimensionPixelSize(
118                     R.styleable.NavigationView_elevation, 0));
119         }
120         ViewCompat.setFitsSystemWindows(this,
121                 a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false));
122 
123         mMaxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0);
124 
125         final ColorStateList itemIconTint;
126         if (a.hasValue(R.styleable.NavigationView_itemIconTint)) {
127             itemIconTint = a.getColorStateList(R.styleable.NavigationView_itemIconTint);
128         } else {
129             itemIconTint = createDefaultColorStateList(android.R.attr.textColorSecondary);
130         }
131 
132         boolean textAppearanceSet = false;
133         int textAppearance = 0;
134         if (a.hasValue(R.styleable.NavigationView_itemTextAppearance)) {
135             textAppearance = a.getResourceId(R.styleable.NavigationView_itemTextAppearance, 0);
136             textAppearanceSet = true;
137         }
138 
139         ColorStateList itemTextColor = null;
140         if (a.hasValue(R.styleable.NavigationView_itemTextColor)) {
141             itemTextColor = a.getColorStateList(R.styleable.NavigationView_itemTextColor);
142         }
143 
144         if (!textAppearanceSet && itemTextColor == null) {
145             // If there isn't a text appearance set, we'll use a default text color
146             itemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);
147         }
148 
149         final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);
150 
151         mMenu.setCallback(new MenuBuilder.Callback() {
152             @Override
153             public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
154                 return mListener != null && mListener.onNavigationItemSelected(item);
155             }
156 
157             @Override
158             public void onMenuModeChange(MenuBuilder menu) {}
159         });
160         mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID);
161         mPresenter.initForMenu(context, mMenu);
162         mPresenter.setItemIconTintList(itemIconTint);
163         if (textAppearanceSet) {
164             mPresenter.setItemTextAppearance(textAppearance);
165         }
166         mPresenter.setItemTextColor(itemTextColor);
167         mPresenter.setItemBackground(itemBackground);
168         mMenu.addMenuPresenter(mPresenter);
169         addView((View) mPresenter.getMenuView(this));
170 
171         if (a.hasValue(R.styleable.NavigationView_menu)) {
172             inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
173         }
174 
175         if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
176             inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
177         }
178 
179         a.recycle();
180     }
181 
182     @Override
onSaveInstanceState()183     protected Parcelable onSaveInstanceState() {
184         Parcelable superState = super.onSaveInstanceState();
185         SavedState state = new SavedState(superState);
186         state.menuState = new Bundle();
187         mMenu.savePresenterStates(state.menuState);
188         return state;
189     }
190 
191     @Override
onRestoreInstanceState(Parcelable savedState)192     protected void onRestoreInstanceState(Parcelable savedState) {
193         if (!(savedState instanceof SavedState)) {
194             super.onRestoreInstanceState(savedState);
195             return;
196         }
197         SavedState state = (SavedState) savedState;
198         super.onRestoreInstanceState(state.getSuperState());
199         mMenu.restorePresenterStates(state.menuState);
200     }
201 
202     /**
203      * Set a listener that will be notified when a menu item is selected.
204      *
205      * @param listener The listener to notify
206      */
setNavigationItemSelectedListener( @ullable OnNavigationItemSelectedListener listener)207     public void setNavigationItemSelectedListener(
208             @Nullable OnNavigationItemSelectedListener listener) {
209         mListener = listener;
210     }
211 
212     @Override
onMeasure(int widthSpec, int heightSpec)213     protected void onMeasure(int widthSpec, int heightSpec) {
214         switch (MeasureSpec.getMode(widthSpec)) {
215             case MeasureSpec.EXACTLY:
216                 // Nothing to do
217                 break;
218             case MeasureSpec.AT_MOST:
219                 widthSpec = MeasureSpec.makeMeasureSpec(
220                         Math.min(MeasureSpec.getSize(widthSpec), mMaxWidth), MeasureSpec.EXACTLY);
221                 break;
222             case MeasureSpec.UNSPECIFIED:
223                 widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
224                 break;
225         }
226         // Let super sort out the height
227         super.onMeasure(widthSpec, heightSpec);
228     }
229 
230     /**
231      * @hide
232      */
233     @RestrictTo(LIBRARY_GROUP)
234     @Override
onInsetsChanged(WindowInsetsCompat insets)235     protected void onInsetsChanged(WindowInsetsCompat insets) {
236         mPresenter.dispatchApplyWindowInsets(insets);
237     }
238 
239     /**
240      * Inflate a menu resource into this navigation view.
241      *
242      * <p>Existing items in the menu will not be modified or removed.</p>
243      *
244      * @param resId ID of a menu resource to inflate
245      */
inflateMenu(int resId)246     public void inflateMenu(int resId) {
247         mPresenter.setUpdateSuspended(true);
248         getMenuInflater().inflate(resId, mMenu);
249         mPresenter.setUpdateSuspended(false);
250         mPresenter.updateMenuView(false);
251     }
252 
253     /**
254      * Returns the {@link Menu} instance associated with this navigation view.
255      */
getMenu()256     public Menu getMenu() {
257         return mMenu;
258     }
259 
260     /**
261      * Inflates a View and add it as a header of the navigation menu.
262      *
263      * @param res The layout resource ID.
264      * @return a newly inflated View.
265      */
inflateHeaderView(@ayoutRes int res)266     public View inflateHeaderView(@LayoutRes int res) {
267         return mPresenter.inflateHeaderView(res);
268     }
269 
270     /**
271      * Adds a View as a header of the navigation menu.
272      *
273      * @param view The view to be added as a header of the navigation menu.
274      */
addHeaderView(@onNull View view)275     public void addHeaderView(@NonNull View view) {
276         mPresenter.addHeaderView(view);
277     }
278 
279     /**
280      * Removes a previously-added header view.
281      *
282      * @param view The view to remove
283      */
removeHeaderView(@onNull View view)284     public void removeHeaderView(@NonNull View view) {
285         mPresenter.removeHeaderView(view);
286     }
287 
288     /**
289      * Gets the number of headers in this NavigationView.
290      *
291      * @return A positive integer representing the number of headers.
292      */
getHeaderCount()293     public int getHeaderCount() {
294         return mPresenter.getHeaderCount();
295     }
296 
297     /**
298      * Gets the header view at the specified position.
299      *
300      * @param index The position at which to get the view from.
301      * @return The header view the specified position or null if the position does not exist in this
302      * NavigationView.
303      */
getHeaderView(int index)304     public View getHeaderView(int index) {
305         return mPresenter.getHeaderView(index);
306     }
307 
308     /**
309      * Returns the tint which is applied to our menu items' icons.
310      *
311      * @see #setItemIconTintList(ColorStateList)
312      *
313      * @attr ref R.styleable#NavigationView_itemIconTint
314      */
315     @Nullable
getItemIconTintList()316     public ColorStateList getItemIconTintList() {
317         return mPresenter.getItemTintList();
318     }
319 
320     /**
321      * Set the tint which is applied to our menu items' icons.
322      *
323      * @param tint the tint to apply.
324      *
325      * @attr ref R.styleable#NavigationView_itemIconTint
326      */
setItemIconTintList(@ullable ColorStateList tint)327     public void setItemIconTintList(@Nullable ColorStateList tint) {
328         mPresenter.setItemIconTintList(tint);
329     }
330 
331     /**
332      * Returns the tint which is applied to our menu items' icons.
333      *
334      * @see #setItemTextColor(ColorStateList)
335      *
336      * @attr ref R.styleable#NavigationView_itemTextColor
337      */
338     @Nullable
getItemTextColor()339     public ColorStateList getItemTextColor() {
340         return mPresenter.getItemTextColor();
341     }
342 
343     /**
344      * Set the text color to be used on our menu items.
345      *
346      * @see #getItemTextColor()
347      *
348      * @attr ref R.styleable#NavigationView_itemTextColor
349      */
setItemTextColor(@ullable ColorStateList textColor)350     public void setItemTextColor(@Nullable ColorStateList textColor) {
351         mPresenter.setItemTextColor(textColor);
352     }
353 
354     /**
355      * Returns the background drawable for our menu items.
356      *
357      * @see #setItemBackgroundResource(int)
358      *
359      * @attr ref R.styleable#NavigationView_itemBackground
360      */
361     @Nullable
getItemBackground()362     public Drawable getItemBackground() {
363         return mPresenter.getItemBackground();
364     }
365 
366     /**
367      * Set the background of our menu items to the given resource.
368      *
369      * @param resId The identifier of the resource.
370      *
371      * @attr ref R.styleable#NavigationView_itemBackground
372      */
setItemBackgroundResource(@rawableRes int resId)373     public void setItemBackgroundResource(@DrawableRes int resId) {
374         setItemBackground(ContextCompat.getDrawable(getContext(), resId));
375     }
376 
377     /**
378      * Set the background of our menu items to a given resource. The resource should refer to
379      * a Drawable object or null to use the default background set on this navigation menu.
380      *
381      * @attr ref R.styleable#NavigationView_itemBackground
382      */
setItemBackground(@ullable Drawable itemBackground)383     public void setItemBackground(@Nullable Drawable itemBackground) {
384         mPresenter.setItemBackground(itemBackground);
385     }
386 
387     /**
388      * Sets the currently checked item in this navigation menu.
389      *
390      * @param id The item ID of the currently checked item.
391      */
setCheckedItem(@dRes int id)392     public void setCheckedItem(@IdRes int id) {
393         MenuItem item = mMenu.findItem(id);
394         if (item != null) {
395             mPresenter.setCheckedItem((MenuItemImpl) item);
396         }
397     }
398 
399     /**
400      * Set the text appearance of the menu items to a given resource.
401      *
402      * @attr ref R.styleable#NavigationView_itemTextAppearance
403      */
setItemTextAppearance(@tyleRes int resId)404     public void setItemTextAppearance(@StyleRes int resId) {
405         mPresenter.setItemTextAppearance(resId);
406     }
407 
getMenuInflater()408     private MenuInflater getMenuInflater() {
409         if (mMenuInflater == null) {
410             mMenuInflater = new SupportMenuInflater(getContext());
411         }
412         return mMenuInflater;
413     }
414 
createDefaultColorStateList(int baseColorThemeAttr)415     private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
416         final TypedValue value = new TypedValue();
417         if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
418             return null;
419         }
420         ColorStateList baseColor = AppCompatResources.getColorStateList(
421                 getContext(), value.resourceId);
422         if (!getContext().getTheme().resolveAttribute(
423                     android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
424             return null;
425         }
426         int colorPrimary = value.data;
427         int defaultColor = baseColor.getDefaultColor();
428         return new ColorStateList(new int[][]{
429                 DISABLED_STATE_SET,
430                 CHECKED_STATE_SET,
431                 EMPTY_STATE_SET
432         }, new int[]{
433                 baseColor.getColorForState(DISABLED_STATE_SET, defaultColor),
434                 colorPrimary,
435                 defaultColor
436         });
437     }
438 
439     /**
440      * Listener for handling events on navigation items.
441      */
442     public interface OnNavigationItemSelectedListener {
443 
444         /**
445          * Called when an item in the navigation menu is selected.
446          *
447          * @param item The selected item
448          *
449          * @return true to display the item as the selected item
450          */
onNavigationItemSelected(@onNull MenuItem item)451         public boolean onNavigationItemSelected(@NonNull MenuItem item);
452     }
453 
454     /**
455      * User interface state that is stored by NavigationView for implementing
456      * onSaveInstanceState().
457      */
458     public static class SavedState extends AbsSavedState {
459         public Bundle menuState;
460 
SavedState(Parcel in, ClassLoader loader)461         public SavedState(Parcel in, ClassLoader loader) {
462             super(in, loader);
463             menuState = in.readBundle(loader);
464         }
465 
SavedState(Parcelable superState)466         public SavedState(Parcelable superState) {
467             super(superState);
468         }
469 
470         @Override
writeToParcel(@onNull Parcel dest, int flags)471         public void writeToParcel(@NonNull Parcel dest, int flags) {
472             super.writeToParcel(dest, flags);
473             dest.writeBundle(menuState);
474         }
475 
476         public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
477             @Override
478             public SavedState createFromParcel(Parcel in, ClassLoader loader) {
479                 return new SavedState(in, loader);
480             }
481 
482             @Override
483             public SavedState createFromParcel(Parcel in) {
484                 return new SavedState(in, null);
485             }
486 
487             @Override
488             public SavedState[] newArray(int size) {
489                 return new SavedState[size];
490             }
491         };
492     }
493 
494 }
495