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