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