BottomNavigationMenuView.java revision 9c4ba2d0f9ffcd7046735eb8b5e647ccbd418855
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.GROUP_ID;
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(GROUP_ID)
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                activateNewButton(itemPosition);
90                mMenu.performItemAction(itemView.getItemData(), mPresenter, 0);
91            }
92        };
93        mTempChildWidths = new int[BottomNavigationMenu.MAX_ITEM_COUNT];
94    }
95
96    @Override
97    public void initialize(MenuBuilder menu) {
98        mMenu = menu;
99        if (mMenu == null) return;
100        if (mMenu.size() > mActiveButton) {
101            mMenu.getItem(mActiveButton).setChecked(true);
102        }
103    }
104
105    @Override
106    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
107        final int width = MeasureSpec.getSize(widthMeasureSpec);
108        final int count = getChildCount();
109
110        final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
111
112        if (mShiftingMode) {
113            final int inactiveCount = count - 1;
114            final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
115            final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
116            final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
117            final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
118            int extra = width - activeWidth - inactiveWidth * inactiveCount;
119            for (int i = 0; i < count; i++) {
120                mTempChildWidths[i] = (i == mActiveButton) ? activeWidth : inactiveWidth;
121                if (extra > 0) {
122                    mTempChildWidths[i]++;
123                    extra--;
124                }
125            }
126        } else {
127            final int maxAvailable = width / count;
128            final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
129            int extra = width - childWidth * count;
130            for (int i = 0; i < count; i++) {
131                mTempChildWidths[i] = childWidth;
132                if (extra > 0) {
133                    mTempChildWidths[i]++;
134                    extra--;
135                }
136            }
137        }
138
139        int totalWidth = 0;
140        for (int i = 0; i < count; i++) {
141            final View child = getChildAt(i);
142            if (child.getVisibility() == GONE) {
143                continue;
144            }
145            child.measure(MeasureSpec.makeMeasureSpec(mTempChildWidths[i], MeasureSpec.EXACTLY),
146                    heightSpec);
147            ViewGroup.LayoutParams params = child.getLayoutParams();
148            params.width = child.getMeasuredWidth();
149            totalWidth += child.getMeasuredWidth();
150        }
151        setMeasuredDimension(
152                ViewCompat.resolveSizeAndState(totalWidth,
153                        MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
154                ViewCompat.resolveSizeAndState(mItemHeight, heightSpec, 0));
155    }
156
157    @Override
158    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
159        final int count = getChildCount();
160        final int width = right - left;
161        final int height = bottom - top;
162        int used = 0;
163        for (int i = 0; i < count; i++) {
164            final View child = getChildAt(i);
165            if (child.getVisibility() == GONE) {
166                continue;
167            }
168            if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
169                child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
170            } else {
171                child.layout(used, 0, child.getMeasuredWidth() + used, height);
172            }
173            used += child.getMeasuredWidth();
174        }
175    }
176
177    @Override
178    public int getWindowAnimations() {
179        return 0;
180    }
181
182    /**
183     * Set the tint which is applied to the menu items' icons.
184     *
185     * @param tint the tint to apply.
186     */
187    public void setIconTintList(ColorStateList tint) {
188        mItemIconTint = tint;
189        if (mButtons == null) return;
190        for (BottomNavigationItemView item : mButtons) {
191            item.setIconTintList(tint);
192        }
193    }
194
195    /**
196     * Returns the tint which is applied to menu items' icons.
197     *
198     * @return The ColorStateList that is used to tint menu items' icons.
199     */
200    @Nullable
201    public ColorStateList getIconTintList() {
202        return mItemIconTint;
203    }
204
205    /**
206     * Set the text color to be used on menu items.
207     *
208     * @param color the ColorStateList used for menu items' text.
209     */
210    public void setItemTextColor(ColorStateList color) {
211        mItemTextColor = color;
212        if (mButtons == null) return;
213        for (BottomNavigationItemView item : mButtons) {
214            item.setTextColor(color);
215        }
216    }
217
218    /**
219     * Returns the text color used on menu items.
220     *
221     * @return the ColorStateList used for menu items' text.
222     */
223    public ColorStateList getItemTextColor() {
224        return mItemTextColor;
225    }
226
227    /**
228     * Sets the resource id to be used for item background.
229     * @param background the resource id of the background.
230     */
231    public void setItemBackgroundRes(int background) {
232        mItemBackgroundRes = background;
233        if (mButtons == null) return;
234        for (BottomNavigationItemView item : mButtons) {
235            item.setItemBackground(background);
236        }
237    }
238
239    /**
240     * Returns the background resource of the menu items.
241     *
242     * @return the resource id of the background.
243     */
244    public int getItemBackgroundRes() {
245        return mItemBackgroundRes;
246    }
247
248    public void setPresenter(BottomNavigationPresenter presenter) {
249        mPresenter = presenter;
250    }
251
252    public void buildMenuView() {
253        if (mButtons != null) {
254            for (BottomNavigationItemView item : mButtons) {
255                sItemPool.release(item);
256            }
257        }
258        removeAllViews();
259        mButtons = new BottomNavigationItemView[mMenu.size()];
260        mShiftingMode = mMenu.size() > 3;
261        for (int i = 0; i < mMenu.size(); i++) {
262            mPresenter.setUpdateSuspended(true);
263            mMenu.getItem(i).setCheckable(true);
264            mPresenter.setUpdateSuspended(false);
265            BottomNavigationItemView child = getNewItem();
266            mButtons[i] = child;
267            child.setIconTintList(mItemIconTint);
268            child.setTextColor(mItemTextColor);
269            child.setItemBackground(mItemBackgroundRes);
270            child.setShiftingMode(mShiftingMode);
271            child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
272            child.setItemPosition(i);
273            child.setOnClickListener(mOnClickListener);
274            addView(child);
275        }
276    }
277
278    public void updateMenuView() {
279        final int menuSize = mMenu.size();
280        if (menuSize != mButtons.length) {
281            // The size has changed. Rebuild menu view from scratch.
282            buildMenuView();
283            return;
284        }
285        for (int i = 0; i < menuSize; i++) {
286            mPresenter.setUpdateSuspended(true);
287            mButtons[i].initialize((MenuItemImpl) mMenu.getItem(i), 0);
288            mPresenter.setUpdateSuspended(false);
289        }
290    }
291
292    private void activateNewButton(int newButton) {
293        if (mActiveButton == newButton) return;
294
295        mAnimationHelper.beginDelayedTransition(this);
296
297        mPresenter.setUpdateSuspended(true);
298        mButtons[mActiveButton].setChecked(false);
299        mButtons[newButton].setChecked(true);
300        mPresenter.setUpdateSuspended(false);
301
302        mActiveButton = newButton;
303    }
304
305    private BottomNavigationItemView getNewItem() {
306        BottomNavigationItemView item = sItemPool.acquire();
307        if (item == null) {
308            item = new BottomNavigationItemView(getContext());
309        }
310        return item;
311    }
312}
313