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 * <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"> 60 * 61 * <!-- Your contents --> 62 * 63 * <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" /> 69 * </android.support.v4.widget.DrawerLayout> 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