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