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