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.widget;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.Context;
22import android.content.res.ColorStateList;
23import android.graphics.drawable.Drawable;
24import android.os.Bundle;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.support.annotation.DrawableRes;
28import android.support.annotation.IdRes;
29import android.support.annotation.LayoutRes;
30import android.support.annotation.NonNull;
31import android.support.annotation.Nullable;
32import android.support.annotation.RestrictTo;
33import android.support.annotation.StyleRes;
34import android.support.design.R;
35import android.support.design.internal.NavigationMenu;
36import android.support.design.internal.NavigationMenuPresenter;
37import android.support.design.internal.ScrimInsetsFrameLayout;
38import android.support.v4.content.ContextCompat;
39import android.support.v4.view.AbsSavedState;
40import android.support.v4.view.ViewCompat;
41import android.support.v4.view.WindowInsetsCompat;
42import android.support.v7.content.res.AppCompatResources;
43import android.support.v7.view.SupportMenuInflater;
44import android.support.v7.view.menu.MenuBuilder;
45import android.support.v7.view.menu.MenuItemImpl;
46import android.support.v7.widget.TintTypedArray;
47import android.util.AttributeSet;
48import android.util.TypedValue;
49import android.view.Menu;
50import android.view.MenuInflater;
51import android.view.MenuItem;
52import 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 */
78public 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
93    public NavigationView(Context context) {
94        this(context, null);
95    }
96
97    public NavigationView(Context context, AttributeSet attrs) {
98        this(context, attrs, 0);
99    }
100
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
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
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     */
207    public void setNavigationItemSelectedListener(
208            @Nullable OnNavigationItemSelectedListener listener) {
209        mListener = listener;
210    }
211
212    @Override
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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
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     */
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
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     */
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     */
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     */
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     */
404    public void setItemTextAppearance(@StyleRes int resId) {
405        mPresenter.setItemTextAppearance(resId);
406    }
407
408    private MenuInflater getMenuInflater() {
409        if (mMenuInflater == null) {
410            mMenuInflater = new SupportMenuInflater(getContext());
411        }
412        return mMenuInflater;
413    }
414
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         */
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
461        public SavedState(Parcel in, ClassLoader loader) {
462            super(in, loader);
463            menuState = in.readBundle(loader);
464        }
465
466        public SavedState(Parcelable superState) {
467            super(superState);
468        }
469
470        @Override
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