1/* 2 * Copyright (C) 2016 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.os.Build; 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.NonNull; 28import android.support.annotation.Nullable; 29import android.support.design.R; 30import android.support.design.internal.BottomNavigationMenu; 31import android.support.design.internal.BottomNavigationMenuView; 32import android.support.design.internal.BottomNavigationPresenter; 33import android.support.v4.content.ContextCompat; 34import android.support.v4.view.AbsSavedState; 35import android.support.v4.view.ViewCompat; 36import android.support.v7.content.res.AppCompatResources; 37import android.support.v7.view.SupportMenuInflater; 38import android.support.v7.view.menu.MenuBuilder; 39import android.support.v7.widget.TintTypedArray; 40import android.util.AttributeSet; 41import android.util.TypedValue; 42import android.view.Gravity; 43import android.view.Menu; 44import android.view.MenuInflater; 45import android.view.MenuItem; 46import android.view.View; 47import android.view.ViewGroup; 48import android.widget.FrameLayout; 49 50/** 51 * <p> 52 * Represents a standard bottom navigation bar for application. It is an implementation of 53 * <a href="https://material.google.com/components/bottom-navigation.html">material design bottom 54 * navigation</a>. 55 * </p> 56 * 57 * <p> 58 * Bottom navigation bars make it easy for users to explore and switch between top-level views in 59 * a single tap. It should be used when application has three to five top-level destinations. 60 * </p> 61 * 62 * <p> 63 * The bar contents can be populated by specifying a menu resource file. Each menu item title, icon 64 * and enabled state will be used for displaying bottom navigation bar items. Menu items can also be 65 * used for programmatically selecting which destination is currently active. It can be done using 66 * {@code MenuItem#setChecked(true)} 67 * </p> 68 * 69 * <pre> 70 * layout resource file: 71 * <android.support.design.widget.BottomNavigationView 72 * xmlns:android="http://schemas.android.com/apk/res/android" 73 * xmlns:app="http://schemas.android.com/apk/res-auto" 74 * android:id="@+id/navigation" 75 * android:layout_width="match_parent" 76 * android:layout_height="56dp" 77 * android:layout_gravity="start" 78 * app:menu="@menu/my_navigation_items" /> 79 * 80 * res/menu/my_navigation_items.xml: 81 * <menu xmlns:android="http://schemas.android.com/apk/res/android"> 82 * <item android:id="@+id/action_search" 83 * android:title="@string/menu_search" 84 * android:icon="@drawable/ic_search" /> 85 * <item android:id="@+id/action_settings" 86 * android:title="@string/menu_settings" 87 * android:icon="@drawable/ic_add" /> 88 * <item android:id="@+id/action_navigation" 89 * android:title="@string/menu_navigation" 90 * android:icon="@drawable/ic_action_navigation_menu" /> 91 * </menu> 92 * </pre> 93 */ 94public class BottomNavigationView extends FrameLayout { 95 96 private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; 97 private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; 98 99 private static final int MENU_PRESENTER_ID = 1; 100 101 private final MenuBuilder mMenu; 102 private final BottomNavigationMenuView mMenuView; 103 private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter(); 104 private MenuInflater mMenuInflater; 105 106 private OnNavigationItemSelectedListener mSelectedListener; 107 private OnNavigationItemReselectedListener mReselectedListener; 108 109 public BottomNavigationView(Context context) { 110 this(context, null); 111 } 112 113 public BottomNavigationView(Context context, AttributeSet attrs) { 114 this(context, attrs, 0); 115 } 116 117 public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) { 118 super(context, attrs, defStyleAttr); 119 120 ThemeUtils.checkAppCompatTheme(context); 121 122 // Create the menu 123 mMenu = new BottomNavigationMenu(context); 124 125 mMenuView = new BottomNavigationMenuView(context); 126 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 127 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 128 params.gravity = Gravity.CENTER; 129 mMenuView.setLayoutParams(params); 130 131 mPresenter.setBottomNavigationMenuView(mMenuView); 132 mPresenter.setId(MENU_PRESENTER_ID); 133 mMenuView.setPresenter(mPresenter); 134 mMenu.addMenuPresenter(mPresenter); 135 mPresenter.initForMenu(getContext(), mMenu); 136 137 // Custom attributes 138 TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, 139 R.styleable.BottomNavigationView, defStyleAttr, 140 R.style.Widget_Design_BottomNavigationView); 141 142 if (a.hasValue(R.styleable.BottomNavigationView_itemIconTint)) { 143 mMenuView.setIconTintList( 144 a.getColorStateList(R.styleable.BottomNavigationView_itemIconTint)); 145 } else { 146 mMenuView.setIconTintList( 147 createDefaultColorStateList(android.R.attr.textColorSecondary)); 148 } 149 if (a.hasValue(R.styleable.BottomNavigationView_itemTextColor)) { 150 mMenuView.setItemTextColor( 151 a.getColorStateList(R.styleable.BottomNavigationView_itemTextColor)); 152 } else { 153 mMenuView.setItemTextColor( 154 createDefaultColorStateList(android.R.attr.textColorSecondary)); 155 } 156 if (a.hasValue(R.styleable.BottomNavigationView_elevation)) { 157 ViewCompat.setElevation(this, a.getDimensionPixelSize( 158 R.styleable.BottomNavigationView_elevation, 0)); 159 } 160 161 int itemBackground = a.getResourceId(R.styleable.BottomNavigationView_itemBackground, 0); 162 mMenuView.setItemBackgroundRes(itemBackground); 163 164 if (a.hasValue(R.styleable.BottomNavigationView_menu)) { 165 inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0)); 166 } 167 a.recycle(); 168 169 addView(mMenuView, params); 170 if (Build.VERSION.SDK_INT < 21) { 171 addCompatibilityTopDivider(context); 172 } 173 174 mMenu.setCallback(new MenuBuilder.Callback() { 175 @Override 176 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { 177 if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) { 178 mReselectedListener.onNavigationItemReselected(item); 179 return true; // item is already selected 180 } 181 return mSelectedListener != null 182 && !mSelectedListener.onNavigationItemSelected(item); 183 } 184 185 @Override 186 public void onMenuModeChange(MenuBuilder menu) {} 187 }); 188 } 189 190 /** 191 * Set a listener that will be notified when a bottom navigation item is selected. This listener 192 * will also be notified when the currently selected item is reselected, unless an 193 * {@link OnNavigationItemReselectedListener} has also been set. 194 * 195 * @param listener The listener to notify 196 * 197 * @see #setOnNavigationItemReselectedListener(OnNavigationItemReselectedListener) 198 */ 199 public void setOnNavigationItemSelectedListener( 200 @Nullable OnNavigationItemSelectedListener listener) { 201 mSelectedListener = listener; 202 } 203 204 /** 205 * Set a listener that will be notified when the currently selected bottom navigation item is 206 * reselected. This does not require an {@link OnNavigationItemSelectedListener} to be set. 207 * 208 * @param listener The listener to notify 209 * 210 * @see #setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener) 211 */ 212 public void setOnNavigationItemReselectedListener( 213 @Nullable OnNavigationItemReselectedListener listener) { 214 mReselectedListener = listener; 215 } 216 217 /** 218 * Returns the {@link Menu} instance associated with this bottom navigation bar. 219 */ 220 @NonNull 221 public Menu getMenu() { 222 return mMenu; 223 } 224 225 /** 226 * Inflate a menu resource into this navigation view. 227 * 228 * <p>Existing items in the menu will not be modified or removed.</p> 229 * 230 * @param resId ID of a menu resource to inflate 231 */ 232 public void inflateMenu(int resId) { 233 mPresenter.setUpdateSuspended(true); 234 getMenuInflater().inflate(resId, mMenu); 235 mPresenter.setUpdateSuspended(false); 236 mPresenter.updateMenuView(true); 237 } 238 239 /** 240 * @return The maximum number of items that can be shown in BottomNavigationView. 241 */ 242 public int getMaxItemCount() { 243 return BottomNavigationMenu.MAX_ITEM_COUNT; 244 } 245 246 /** 247 * Returns the tint which is applied to our menu items' icons. 248 * 249 * @see #setItemIconTintList(ColorStateList) 250 * 251 * @attr ref R.styleable#BottomNavigationView_itemIconTint 252 */ 253 @Nullable 254 public ColorStateList getItemIconTintList() { 255 return mMenuView.getIconTintList(); 256 } 257 258 /** 259 * Set the tint which is applied to our menu items' icons. 260 * 261 * @param tint the tint to apply. 262 * 263 * @attr ref R.styleable#BottomNavigationView_itemIconTint 264 */ 265 public void setItemIconTintList(@Nullable ColorStateList tint) { 266 mMenuView.setIconTintList(tint); 267 } 268 269 /** 270 * Returns colors used for the different states (normal, selected, focused, etc.) of the menu 271 * item text. 272 * 273 * @see #setItemTextColor(ColorStateList) 274 * 275 * @return the ColorStateList of colors used for the different states of the menu items text. 276 * 277 * @attr ref R.styleable#BottomNavigationView_itemTextColor 278 */ 279 @Nullable 280 public ColorStateList getItemTextColor() { 281 return mMenuView.getItemTextColor(); 282 } 283 284 /** 285 * Set the colors to use for the different states (normal, selected, focused, etc.) of the menu 286 * item text. 287 * 288 * @see #getItemTextColor() 289 * 290 * @attr ref R.styleable#BottomNavigationView_itemTextColor 291 */ 292 public void setItemTextColor(@Nullable ColorStateList textColor) { 293 mMenuView.setItemTextColor(textColor); 294 } 295 296 /** 297 * Returns the background resource of the menu items. 298 * 299 * @see #setItemBackgroundResource(int) 300 * 301 * @attr ref R.styleable#BottomNavigationView_itemBackground 302 */ 303 @DrawableRes 304 public int getItemBackgroundResource() { 305 return mMenuView.getItemBackgroundRes(); 306 } 307 308 /** 309 * Set the background of our menu items to the given resource. 310 * 311 * @param resId The identifier of the resource. 312 * 313 * @attr ref R.styleable#BottomNavigationView_itemBackground 314 */ 315 public void setItemBackgroundResource(@DrawableRes int resId) { 316 mMenuView.setItemBackgroundRes(resId); 317 } 318 319 /** 320 * Returns the currently selected menu item ID, or zero if there is no menu. 321 * 322 * @see #setSelectedItemId(int) 323 */ 324 @IdRes 325 public int getSelectedItemId() { 326 return mMenuView.getSelectedItemId(); 327 } 328 329 /** 330 * Set the selected menu item ID. This behaves the same as tapping on an item. 331 * 332 * @param itemId The menu item ID. If no item has this ID, the current selection is unchanged. 333 * 334 * @see #getSelectedItemId() 335 */ 336 public void setSelectedItemId(@IdRes int itemId) { 337 MenuItem item = mMenu.findItem(itemId); 338 if (item != null) { 339 if (!mMenu.performItemAction(item, mPresenter, 0)) { 340 item.setChecked(true); 341 } 342 } 343 } 344 345 /** 346 * Listener for handling selection events on bottom navigation items. 347 */ 348 public interface OnNavigationItemSelectedListener { 349 350 /** 351 * Called when an item in the bottom navigation menu is selected. 352 * 353 * @param item The selected item 354 * 355 * @return true to display the item as the selected item and false if the item should not 356 * be selected. Consider setting non-selectable items as disabled preemptively to 357 * make them appear non-interactive. 358 */ 359 boolean onNavigationItemSelected(@NonNull MenuItem item); 360 } 361 362 /** 363 * Listener for handling reselection events on bottom navigation items. 364 */ 365 public interface OnNavigationItemReselectedListener { 366 367 /** 368 * Called when the currently selected item in the bottom navigation menu is selected again. 369 * 370 * @param item The selected item 371 */ 372 void onNavigationItemReselected(@NonNull MenuItem item); 373 } 374 375 private void addCompatibilityTopDivider(Context context) { 376 View divider = new View(context); 377 divider.setBackgroundColor( 378 ContextCompat.getColor(context, R.color.design_bottom_navigation_shadow_color)); 379 FrameLayout.LayoutParams dividerParams = new FrameLayout.LayoutParams( 380 ViewGroup.LayoutParams.MATCH_PARENT, 381 getResources().getDimensionPixelSize( 382 R.dimen.design_bottom_navigation_shadow_height)); 383 divider.setLayoutParams(dividerParams); 384 addView(divider); 385 } 386 387 private MenuInflater getMenuInflater() { 388 if (mMenuInflater == null) { 389 mMenuInflater = new SupportMenuInflater(getContext()); 390 } 391 return mMenuInflater; 392 } 393 394 private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) { 395 final TypedValue value = new TypedValue(); 396 if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) { 397 return null; 398 } 399 ColorStateList baseColor = AppCompatResources.getColorStateList( 400 getContext(), value.resourceId); 401 if (!getContext().getTheme().resolveAttribute( 402 android.support.v7.appcompat.R.attr.colorPrimary, value, true)) { 403 return null; 404 } 405 int colorPrimary = value.data; 406 int defaultColor = baseColor.getDefaultColor(); 407 return new ColorStateList(new int[][]{ 408 DISABLED_STATE_SET, 409 CHECKED_STATE_SET, 410 EMPTY_STATE_SET 411 }, new int[]{ 412 baseColor.getColorForState(DISABLED_STATE_SET, defaultColor), 413 colorPrimary, 414 defaultColor 415 }); 416 } 417 418 @Override 419 protected Parcelable onSaveInstanceState() { 420 Parcelable superState = super.onSaveInstanceState(); 421 SavedState savedState = new SavedState(superState); 422 savedState.menuPresenterState = new Bundle(); 423 mMenu.savePresenterStates(savedState.menuPresenterState); 424 return savedState; 425 } 426 427 @Override 428 protected void onRestoreInstanceState(Parcelable state) { 429 if (!(state instanceof SavedState)) { 430 super.onRestoreInstanceState(state); 431 return; 432 } 433 SavedState savedState = (SavedState) state; 434 super.onRestoreInstanceState(savedState.getSuperState()); 435 mMenu.restorePresenterStates(savedState.menuPresenterState); 436 } 437 438 static class SavedState extends AbsSavedState { 439 Bundle menuPresenterState; 440 441 public SavedState(Parcelable superState) { 442 super(superState); 443 } 444 445 public SavedState(Parcel source, ClassLoader loader) { 446 super(source, loader); 447 readFromParcel(source, loader); 448 } 449 450 @Override 451 public void writeToParcel(@NonNull Parcel out, int flags) { 452 super.writeToParcel(out, flags); 453 out.writeBundle(menuPresenterState); 454 } 455 456 private void readFromParcel(Parcel in, ClassLoader loader) { 457 menuPresenterState = in.readBundle(loader); 458 } 459 460 public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() { 461 @Override 462 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 463 return new SavedState(in, loader); 464 } 465 466 @Override 467 public SavedState createFromParcel(Parcel in) { 468 return new SavedState(in, null); 469 } 470 471 @Override 472 public SavedState[] newArray(int size) { 473 return new SavedState[size]; 474 } 475 }; 476 } 477} 478