Home | History | Annotate | Download | only in widget
      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 
     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