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 * <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"> 66 * 67 * <!-- Your contents --> 68 * 69 * <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" /> 75 * </android.support.v4.widget.DrawerLayout> 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