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