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