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