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