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 * &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 */
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