BottomNavigationView.java revision b01c03a178cb74302b83e20820fb98a817b47e3e
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.NonNull; 27import android.support.annotation.Nullable; 28import android.support.design.R; 29import android.support.design.internal.BottomNavigationMenu; 30import android.support.design.internal.BottomNavigationMenuView; 31import android.support.design.internal.BottomNavigationPresenter; 32import android.support.v4.content.ContextCompat; 33import android.support.v4.os.ParcelableCompat; 34import android.support.v4.os.ParcelableCompatCreatorCallbacks; 35import android.support.v4.view.AbsSavedState; 36import android.support.v4.view.ViewCompat; 37import android.support.v7.content.res.AppCompatResources; 38import android.support.v7.view.SupportMenuInflater; 39import android.support.v7.view.menu.MenuBuilder; 40import android.support.v7.widget.TintTypedArray; 41import android.util.AttributeSet; 42import android.util.TypedValue; 43import android.view.Gravity; 44import android.view.Menu; 45import android.view.MenuInflater; 46import android.view.MenuItem; 47import android.view.View; 48import android.view.ViewGroup; 49import android.widget.FrameLayout; 50 51/** 52 * <p> 53 * Represents a standard bottom navigation bar for application. It is an implementation of 54 * <a href="https://material.google.com/components/bottom-navigation.html">material design bottom 55 * navigation</a>. 56 * </p> 57 * 58 * <p> 59 * Bottom navigation bars make it easy for users to explore and switch between top-level views in 60 * a single tap. It should be used when application has three to five top-level destinations. 61 * </p> 62 * 63 * <p> 64 * The bar contents can be populated by specifying a menu resource file. Each menu item title, icon 65 * and enabled state will be used for displaying bottom navigation bar items. Menu items can also be 66 * used for programmatically selecting which destination is currently active. It can be done using 67 * {@code MenuItem#setChecked(true)} 68 * </p> 69 * 70 * <pre> 71 * layout resource file: 72 * <android.support.design.widget.BottomNavigationView 73 * xmlns:android="http://schemas.android.com/apk/res/android" 74 * xmlns:design="http://schema.android.com/apk/res/android.support.design" 75 * android:id="@+id/navigation" 76 * android:layout_width="match_parent" 77 * android:layout_height="56dp" 78 * android:layout_gravity="start" 79 * design:menu="@menu/my_navigation_items" /> 80 * 81 * res/menu/my_navigation_items.xml: 82 * <menu xmlns:android="http://schemas.android.com/apk/res/android"> 83 * <item android:id="@+id/action_search" 84 * android:title="@string/menu_search" 85 * android:icon="@drawable/ic_search" /> 86 * <item android:id="@+id/action_settings" 87 * android:title="@string/menu_settings" 88 * android:icon="@drawable/ic_add" /> 89 * <item android:id="@+id/action_navigation" 90 * android:title="@string/menu_navigation" 91 * android:icon="@drawable/ic_action_navigation_menu" /> 92 * </menu> 93 * </pre> 94 */ 95public class BottomNavigationView extends FrameLayout { 96 97 private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; 98 private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; 99 100 private static final int MENU_PRESENTER_ID = 1; 101 102 private final MenuBuilder mMenu; 103 private final BottomNavigationMenuView mMenuView; 104 private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter(); 105 private MenuInflater mMenuInflater; 106 107 private OnNavigationItemSelectedListener mListener; 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 return mListener != null && !mListener.onNavigationItemSelected(item); 178 } 179 180 @Override 181 public void onMenuModeChange(MenuBuilder menu) {} 182 }); 183 } 184 185 /** 186 * Set a listener that will be notified when a bottom navigation item is selected. 187 * 188 * @param listener The listener to notify 189 */ 190 public void setOnNavigationItemSelectedListener( 191 @Nullable OnNavigationItemSelectedListener listener) { 192 mListener = listener; 193 } 194 195 /** 196 * Returns the {@link Menu} instance associated with this bottom navigation bar. 197 */ 198 @NonNull 199 public Menu getMenu() { 200 return mMenu; 201 } 202 203 /** 204 * Inflate a menu resource into this navigation view. 205 * 206 * <p>Existing items in the menu will not be modified or removed.</p> 207 * 208 * @param resId ID of a menu resource to inflate 209 */ 210 public void inflateMenu(int resId) { 211 mPresenter.setUpdateSuspended(true); 212 getMenuInflater().inflate(resId, mMenu); 213 mPresenter.setUpdateSuspended(false); 214 mPresenter.updateMenuView(true); 215 } 216 217 /** 218 * @return The maximum number of items that can be shown in BottomNavigationView. 219 */ 220 public int getMaxItemCount() { 221 return BottomNavigationMenu.MAX_ITEM_COUNT; 222 } 223 224 /** 225 * Returns the tint which is applied to our menu items' icons. 226 * 227 * @see #setItemIconTintList(ColorStateList) 228 * 229 * @attr ref R.styleable#BottomNavigationView_itemIconTint 230 */ 231 @Nullable 232 public ColorStateList getItemIconTintList() { 233 return mMenuView.getIconTintList(); 234 } 235 236 /** 237 * Set the tint which is applied to our menu items' icons. 238 * 239 * @param tint the tint to apply. 240 * 241 * @attr ref R.styleable#BottomNavigationView_itemIconTint 242 */ 243 public void setItemIconTintList(@Nullable ColorStateList tint) { 244 mMenuView.setIconTintList(tint); 245 } 246 247 /** 248 * Returns colors used for the different states (normal, selected, focused, etc.) of the menu 249 * item text. 250 * 251 * @see #setItemTextColor(ColorStateList) 252 * 253 * @return the ColorStateList of colors used for the different states of the menu items text. 254 * 255 * @attr ref R.styleable#BottomNavigationView_itemTextColor 256 */ 257 @Nullable 258 public ColorStateList getItemTextColor() { 259 return mMenuView.getItemTextColor(); 260 } 261 262 /** 263 * Set the colors to use for the different states (normal, selected, focused, etc.) of the menu 264 * item text. 265 * 266 * @see #getItemTextColor() 267 * 268 * @attr ref R.styleable#BottomNavigationView_itemTextColor 269 */ 270 public void setItemTextColor(@Nullable ColorStateList textColor) { 271 mMenuView.setItemTextColor(textColor); 272 } 273 274 /** 275 * Returns the background resource of the menu items. 276 * 277 * @see #setItemBackgroundResource(int) 278 * 279 * @attr ref R.styleable#BottomNavigationView_itemBackground 280 */ 281 @DrawableRes 282 public int getItemBackgroundResource() { 283 return mMenuView.getItemBackgroundRes(); 284 } 285 286 /** 287 * Set the background of our menu items to the given resource. 288 * 289 * @param resId The identifier of the resource. 290 * 291 * @attr ref R.styleable#BottomNavigationView_itemBackground 292 */ 293 public void setItemBackgroundResource(@DrawableRes int resId) { 294 mMenuView.setItemBackgroundRes(resId); 295 } 296 297 /** 298 * Listener for handling events on bottom navigation items. 299 */ 300 public interface OnNavigationItemSelectedListener { 301 302 /** 303 * Called when an item in the bottom navigation menu is selected. 304 * 305 * @param item The selected item 306 * 307 * @return true to display the item as the selected item and false if the item should not 308 * be selected. Consider setting non-selectable items as disabled preemptively to 309 * make them appear non-interactive. 310 */ 311 boolean onNavigationItemSelected(@NonNull MenuItem item); 312 } 313 314 private void addCompatibilityTopDivider(Context context) { 315 View divider = new View(context); 316 divider.setBackgroundColor( 317 ContextCompat.getColor(context, R.color.design_bottom_navigation_shadow_color)); 318 FrameLayout.LayoutParams dividerParams = new FrameLayout.LayoutParams( 319 ViewGroup.LayoutParams.MATCH_PARENT, 320 getResources().getDimensionPixelSize( 321 R.dimen.design_bottom_navigation_shadow_height)); 322 divider.setLayoutParams(dividerParams); 323 addView(divider); 324 } 325 326 private MenuInflater getMenuInflater() { 327 if (mMenuInflater == null) { 328 mMenuInflater = new SupportMenuInflater(getContext()); 329 } 330 return mMenuInflater; 331 } 332 333 private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) { 334 final TypedValue value = new TypedValue(); 335 if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) { 336 return null; 337 } 338 ColorStateList baseColor = AppCompatResources.getColorStateList( 339 getContext(), value.resourceId); 340 if (!getContext().getTheme().resolveAttribute( 341 android.support.v7.appcompat.R.attr.colorPrimary, value, true)) { 342 return null; 343 } 344 int colorPrimary = value.data; 345 int defaultColor = baseColor.getDefaultColor(); 346 return new ColorStateList(new int[][]{ 347 DISABLED_STATE_SET, 348 CHECKED_STATE_SET, 349 EMPTY_STATE_SET 350 }, new int[]{ 351 baseColor.getColorForState(DISABLED_STATE_SET, defaultColor), 352 colorPrimary, 353 defaultColor 354 }); 355 } 356 357 @Override 358 protected Parcelable onSaveInstanceState() { 359 Parcelable superState = super.onSaveInstanceState(); 360 SavedState savedState = new SavedState(superState); 361 savedState.menuPresenterState = new Bundle(); 362 mMenu.savePresenterStates(savedState.menuPresenterState); 363 return savedState; 364 } 365 366 @Override 367 protected void onRestoreInstanceState(Parcelable state) { 368 if (!(state instanceof SavedState)) { 369 super.onRestoreInstanceState(state); 370 return; 371 } 372 SavedState savedState = (SavedState) state; 373 super.onRestoreInstanceState(savedState.getSuperState()); 374 mMenu.restorePresenterStates(savedState.menuPresenterState); 375 } 376 377 static class SavedState extends AbsSavedState { 378 Bundle menuPresenterState; 379 380 public SavedState(Parcelable superState) { 381 super(superState); 382 } 383 384 public SavedState(Parcel source, ClassLoader loader) { 385 super(source, loader); 386 readFromParcel(source, loader); 387 } 388 389 @Override 390 public void writeToParcel(@NonNull Parcel out, int flags) { 391 super.writeToParcel(out, flags); 392 out.writeBundle(menuPresenterState); 393 } 394 395 private void readFromParcel(Parcel in, ClassLoader loader) { 396 menuPresenterState = in.readBundle(loader); 397 } 398 399 public static final Creator<SavedState> CREATOR = 400 ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { 401 @Override 402 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 403 return new SavedState(in, loader); 404 } 405 406 @Override 407 public SavedState[] newArray(int size) { 408 return new SavedState[size]; 409 } 410 }); 411 } 412} 413