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 * &lt;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" /&gt;
80 *
81 * res/menu/my_navigation_items.xml:
82 * &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"&gt;
83 *     &lt;item android:id="@+id/action_search"
84 *          android:title="@string/menu_search"
85 *          android:icon="@drawable/ic_search" /&gt;
86 *     &lt;item android:id="@+id/action_settings"
87 *          android:title="@string/menu_settings"
88 *          android:icon="@drawable/ic_add" /&gt;
89 *     &lt;item android:id="@+id/action_navigation"
90 *          android:title="@string/menu_navigation"
91 *          android:icon="@drawable/ic_action_navigation_menu" /&gt;
92 * &lt;/menu&gt;
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