BottomNavigationMenuView.java revision fa0f82f629bf95681c14ed559922f77a3030aa18
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.internal;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.Context;
22import android.content.res.ColorStateList;
23import android.content.res.Resources;
24import android.support.annotation.Nullable;
25import android.support.annotation.RestrictTo;
26import android.support.design.R;
27import android.support.transition.AutoTransition;
28import android.support.transition.TransitionManager;
29import android.support.transition.TransitionSet;
30import android.support.v4.util.Pools;
31import android.support.v4.view.ViewCompat;
32import android.support.v4.view.animation.FastOutSlowInInterpolator;
33import android.support.v7.view.menu.MenuBuilder;
34import android.support.v7.view.menu.MenuItemImpl;
35import android.support.v7.view.menu.MenuView;
36import android.util.AttributeSet;
37import android.view.MenuItem;
38import android.view.View;
39import android.view.ViewGroup;
40
41/**
42 * @hide For internal use only.
43 */
44@RestrictTo(LIBRARY_GROUP)
45public class BottomNavigationMenuView extends ViewGroup implements MenuView {
46    private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
47
48    private final TransitionSet mSet;
49    private final int mInactiveItemMaxWidth;
50    private final int mInactiveItemMinWidth;
51    private final int mActiveItemMaxWidth;
52    private final int mItemHeight;
53    private final OnClickListener mOnClickListener;
54    private static final Pools.Pool<BottomNavigationItemView> sItemPool =
55            new Pools.SynchronizedPool<>(5);
56
57    private boolean mShiftingMode = true;
58
59    private BottomNavigationItemView[] mButtons;
60    private int mSelectedItemId = 0;
61    private int mSelectedItemPosition = 0;
62    private ColorStateList mItemIconTint;
63    private ColorStateList mItemTextColor;
64    private int mItemBackgroundRes;
65    private int[] mTempChildWidths;
66
67    private BottomNavigationPresenter mPresenter;
68    private MenuBuilder mMenu;
69
70    public BottomNavigationMenuView(Context context) {
71        this(context, null);
72    }
73
74    public BottomNavigationMenuView(Context context, AttributeSet attrs) {
75        super(context, attrs);
76        final Resources res = getResources();
77        mInactiveItemMaxWidth = res.getDimensionPixelSize(
78                R.dimen.design_bottom_navigation_item_max_width);
79        mInactiveItemMinWidth = res.getDimensionPixelSize(
80                R.dimen.design_bottom_navigation_item_min_width);
81        mActiveItemMaxWidth = res.getDimensionPixelSize(
82                R.dimen.design_bottom_navigation_active_item_max_width);
83        mItemHeight = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_height);
84
85        mSet = new AutoTransition();
86        mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
87        mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
88        mSet.setInterpolator(new FastOutSlowInInterpolator());
89        mSet.addTransition(new TextScale());
90
91        mOnClickListener = new OnClickListener() {
92            @Override
93            public void onClick(View v) {
94                final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
95                final int itemPosition = itemView.getItemPosition();
96                if (!mMenu.performItemAction(itemView.getItemData(), mPresenter, 0)) {
97                    activateNewButton(itemPosition);
98                }
99            }
100        };
101        mTempChildWidths = new int[BottomNavigationMenu.MAX_ITEM_COUNT];
102    }
103
104    @Override
105    public void initialize(MenuBuilder menu) {
106        mMenu = menu;
107    }
108
109    @Override
110    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
111        final int width = MeasureSpec.getSize(widthMeasureSpec);
112        final int count = getChildCount();
113
114        final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
115
116        if (mShiftingMode) {
117            final int inactiveCount = count - 1;
118            final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
119            final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
120            final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
121            final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
122            int extra = width - activeWidth - inactiveWidth * inactiveCount;
123            for (int i = 0; i < count; i++) {
124                mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
125                if (extra > 0) {
126                    mTempChildWidths[i]++;
127                    extra--;
128                }
129            }
130        } else {
131            final int maxAvailable = width / (count == 0 ? 1 : count);
132            final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
133            int extra = width - childWidth * count;
134            for (int i = 0; i < count; i++) {
135                mTempChildWidths[i] = childWidth;
136                if (extra > 0) {
137                    mTempChildWidths[i]++;
138                    extra--;
139                }
140            }
141        }
142
143        int totalWidth = 0;
144        for (int i = 0; i < count; i++) {
145            final View child = getChildAt(i);
146            if (child.getVisibility() == GONE) {
147                continue;
148            }
149            child.measure(MeasureSpec.makeMeasureSpec(mTempChildWidths[i], MeasureSpec.EXACTLY),
150                    heightSpec);
151            ViewGroup.LayoutParams params = child.getLayoutParams();
152            params.width = child.getMeasuredWidth();
153            totalWidth += child.getMeasuredWidth();
154        }
155        setMeasuredDimension(
156                View.resolveSizeAndState(totalWidth,
157                        MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
158                View.resolveSizeAndState(mItemHeight, heightSpec, 0));
159    }
160
161    @Override
162    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
163        final int count = getChildCount();
164        final int width = right - left;
165        final int height = bottom - top;
166        int used = 0;
167        for (int i = 0; i < count; i++) {
168            final View child = getChildAt(i);
169            if (child.getVisibility() == GONE) {
170                continue;
171            }
172            if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
173                child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
174            } else {
175                child.layout(used, 0, child.getMeasuredWidth() + used, height);
176            }
177            used += child.getMeasuredWidth();
178        }
179    }
180
181    @Override
182    public int getWindowAnimations() {
183        return 0;
184    }
185
186    /**
187     * Sets the tint which is applied to the menu items' icons.
188     *
189     * @param tint the tint to apply
190     */
191    public void setIconTintList(ColorStateList tint) {
192        mItemIconTint = tint;
193        if (mButtons == null) return;
194        for (BottomNavigationItemView item : mButtons) {
195            item.setIconTintList(tint);
196        }
197    }
198
199    /**
200     * Returns the tint which is applied to menu items' icons.
201     *
202     * @return the ColorStateList that is used to tint menu items' icons
203     */
204    @Nullable
205    public ColorStateList getIconTintList() {
206        return mItemIconTint;
207    }
208
209    /**
210     * Sets the text color to be used on menu items.
211     *
212     * @param color the ColorStateList used for menu items' text.
213     */
214    public void setItemTextColor(ColorStateList color) {
215        mItemTextColor = color;
216        if (mButtons == null) return;
217        for (BottomNavigationItemView item : mButtons) {
218            item.setTextColor(color);
219        }
220    }
221
222    /**
223     * Returns the text color used on menu items.
224     *
225     * @return the ColorStateList used for menu items' text
226     */
227    public ColorStateList getItemTextColor() {
228        return mItemTextColor;
229    }
230
231    /**
232     * Sets the resource ID to be used for item background.
233     *
234     * @param background the resource ID of the background
235     */
236    public void setItemBackgroundRes(int background) {
237        mItemBackgroundRes = background;
238        if (mButtons == null) return;
239        for (BottomNavigationItemView item : mButtons) {
240            item.setItemBackground(background);
241        }
242    }
243
244    /**
245     * Returns the resource ID for the background of the menu items.
246     *
247     * @return the resource ID for the background
248     */
249    public int getItemBackgroundRes() {
250        return mItemBackgroundRes;
251    }
252
253    public void setPresenter(BottomNavigationPresenter presenter) {
254        mPresenter = presenter;
255    }
256
257    public void buildMenuView() {
258        if (mButtons != null) {
259            for (BottomNavigationItemView item : mButtons) {
260                sItemPool.release(item);
261            }
262        }
263        removeAllViews();
264        if (mMenu.size() == 0) {
265            mButtons = null;
266            return;
267        }
268        mButtons = new BottomNavigationItemView[mMenu.size()];
269        mShiftingMode = mMenu.size() > 3;
270        for (int i = 0; i < mMenu.size(); i++) {
271            mPresenter.setUpdateSuspended(true);
272            mMenu.getItem(i).setCheckable(true);
273            mPresenter.setUpdateSuspended(false);
274            BottomNavigationItemView child = getNewItem();
275            mButtons[i] = child;
276            child.setIconTintList(mItemIconTint);
277            child.setTextColor(mItemTextColor);
278            child.setItemBackground(mItemBackgroundRes);
279            child.setShiftingMode(mShiftingMode);
280            child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
281            child.setItemPosition(i);
282            child.setOnClickListener(mOnClickListener);
283            addView(child);
284        }
285        mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
286        mMenu.getItem(mSelectedItemPosition).setChecked(true);
287    }
288
289    public void updateMenuView() {
290        final int menuSize = mMenu.size();
291        if (menuSize != mButtons.length) {
292            // The size has changed. Rebuild menu view from scratch.
293            buildMenuView();
294            return;
295        }
296        for (int i = 0; i < menuSize; i++) {
297            mPresenter.setUpdateSuspended(true);
298            MenuItem item = mMenu.getItem(i);
299            if (item.isChecked()) {
300                mSelectedItemId = item.getItemId();
301                mSelectedItemPosition = i;
302            }
303            mButtons[i].initialize((MenuItemImpl) item, 0);
304            mPresenter.setUpdateSuspended(false);
305        }
306    }
307
308    private void activateNewButton(int newButton) {
309        if (mSelectedItemPosition == newButton) {
310            return;
311        }
312
313        TransitionManager.beginDelayedTransition(this, mSet);
314
315        mMenu.getItem(newButton).setChecked(true);
316
317        mSelectedItemPosition = newButton;
318    }
319
320    private BottomNavigationItemView getNewItem() {
321        BottomNavigationItemView item = sItemPool.acquire();
322        if (item == null) {
323            item = new BottomNavigationItemView(getContext());
324        }
325        return item;
326    }
327
328    int getSelectedItemId() {
329        return mSelectedItemId;
330    }
331
332    void tryRestoreSelectedItemId(int itemId) {
333        final int size = mMenu.size();
334        for (int i = 0; i < size; i++) {
335            MenuItem item = mMenu.getItem(i);
336            if (itemId == item.getItemId()) {
337                mSelectedItemId = itemId;
338                mSelectedItemPosition = i;
339                item.setChecked(true);
340                break;
341            }
342        }
343    }
344}
345