• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.support.design.widget;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.os.Build;
22 import android.os.Bundle;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.support.annotation.DrawableRes;
26 import android.support.annotation.IdRes;
27 import android.support.annotation.NonNull;
28 import android.support.annotation.Nullable;
29 import android.support.design.R;
30 import android.support.design.internal.BottomNavigationMenu;
31 import android.support.design.internal.BottomNavigationMenuView;
32 import android.support.design.internal.BottomNavigationPresenter;
33 import android.support.v4.content.ContextCompat;
34 import android.support.v4.view.AbsSavedState;
35 import android.support.v4.view.ViewCompat;
36 import android.support.v7.content.res.AppCompatResources;
37 import android.support.v7.view.SupportMenuInflater;
38 import android.support.v7.view.menu.MenuBuilder;
39 import android.support.v7.widget.TintTypedArray;
40 import android.util.AttributeSet;
41 import android.util.TypedValue;
42 import android.view.Gravity;
43 import android.view.Menu;
44 import android.view.MenuInflater;
45 import android.view.MenuItem;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import 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  * &lt;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" /&gt;
79  *
80  * res/menu/my_navigation_items.xml:
81  * &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"&gt;
82  *     &lt;item android:id="@+id/action_search"
83  *          android:title="@string/menu_search"
84  *          android:icon="@drawable/ic_search" /&gt;
85  *     &lt;item android:id="@+id/action_settings"
86  *          android:title="@string/menu_settings"
87  *          android:icon="@drawable/ic_add" /&gt;
88  *     &lt;item android:id="@+id/action_navigation"
89  *          android:title="@string/menu_navigation"
90  *          android:icon="@drawable/ic_action_navigation_menu" /&gt;
91  * &lt;/menu&gt;
92  * </pre>
93  */
94 public 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 
BottomNavigationView(Context context)109     public BottomNavigationView(Context context) {
110         this(context, null);
111     }
112 
BottomNavigationView(Context context, AttributeSet attrs)113     public BottomNavigationView(Context context, AttributeSet attrs) {
114         this(context, attrs, 0);
115     }
116 
BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr)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      */
setOnNavigationItemSelectedListener( @ullable OnNavigationItemSelectedListener listener)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      */
setOnNavigationItemReselectedListener( @ullable OnNavigationItemReselectedListener listener)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
getMenu()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      */
inflateMenu(int resId)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      */
getMaxItemCount()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
getItemIconTintList()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      */
setItemIconTintList(@ullable ColorStateList tint)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
getItemTextColor()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      */
setItemTextColor(@ullable ColorStateList textColor)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
getItemBackgroundResource()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      */
setItemBackgroundResource(@rawableRes int resId)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
getSelectedItemId()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      */
setSelectedItemId(@dRes int itemId)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          */
onNavigationItemSelected(@onNull MenuItem item)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          */
onNavigationItemReselected(@onNull MenuItem item)372         void onNavigationItemReselected(@NonNull MenuItem item);
373     }
374 
addCompatibilityTopDivider(Context context)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 
getMenuInflater()387     private MenuInflater getMenuInflater() {
388         if (mMenuInflater == null) {
389             mMenuInflater = new SupportMenuInflater(getContext());
390         }
391         return mMenuInflater;
392     }
393 
createDefaultColorStateList(int baseColorThemeAttr)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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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 
SavedState(Parcelable superState)441         public SavedState(Parcelable superState) {
442             super(superState);
443         }
444 
SavedState(Parcel source, ClassLoader loader)445         public SavedState(Parcel source, ClassLoader loader) {
446             super(source, loader);
447             readFromParcel(source, loader);
448         }
449 
450         @Override
writeToParcel(@onNull Parcel out, int flags)451         public void writeToParcel(@NonNull Parcel out, int flags) {
452             super.writeToParcel(out, flags);
453             out.writeBundle(menuPresenterState);
454         }
455 
readFromParcel(Parcel in, ClassLoader loader)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