afohrman 2f2ba0ce3d Add "enabled" suffix to itemHorizontalTranslation attribute name.
The suffix makes it clearer that the attribute is boolean.

This attribute was introduced in an alpha release, so it is fine to rename.

PiperOrigin-RevId: 193964290
2018-04-30 11:56:12 -04:00

561 lines
19 KiB
Java

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.material.bottomnavigation;
import com.google.android.material.R;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.support.annotation.Dimension;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.StyleRes;
import com.google.android.material.internal.TextScale;
import android.support.transition.AutoTransition;
import android.support.transition.TransitionManager;
import android.support.transition.TransitionSet;
import android.support.v4.util.Pools;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.view.menu.MenuBuilder;
import android.support.v7.view.menu.MenuItemImpl;
import android.support.v7.view.menu.MenuView;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
/** @hide For internal use only. */
@RestrictTo(LIBRARY_GROUP)
public class BottomNavigationMenuView extends ViewGroup implements MenuView {
private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
private final TransitionSet set;
private final int inactiveItemMaxWidth;
private final int inactiveItemMinWidth;
private final int activeItemMaxWidth;
private final int activeItemMinWidth;
private final int itemHeight;
private final OnClickListener onClickListener;
private final Pools.Pool<BottomNavigationItemView> itemPool = new Pools.SynchronizedPool<>(5);
private boolean itemHorizontalTranslationEnabled;
@LabelVisibilityMode private int labelVisibilityMode;
private BottomNavigationItemView[] buttons;
private int selectedItemId = 0;
private int selectedItemPosition = 0;
private ColorStateList itemIconTint;
@Dimension private int itemIconSize;
private ColorStateList itemTextColorFromUser;
private final ColorStateList itemTextColorDefault;
@StyleRes private int itemTextAppearanceInactive;
@StyleRes private int itemTextAppearanceActive;
private int itemBackgroundRes;
private int[] tempChildWidths;
private BottomNavigationPresenter presenter;
private MenuBuilder menu;
public BottomNavigationMenuView(Context context) {
this(context, null);
}
public BottomNavigationMenuView(Context context, AttributeSet attrs) {
super(context, attrs);
final Resources res = getResources();
inactiveItemMaxWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_item_max_width);
inactiveItemMinWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_item_min_width);
activeItemMaxWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_active_item_max_width);
activeItemMinWidth =
res.getDimensionPixelSize(R.dimen.design_bottom_navigation_active_item_min_width);
itemHeight = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_height);
itemTextColorDefault = createDefaultColorStateList(android.R.attr.textColorSecondary);
set = new AutoTransition();
set.setOrdering(TransitionSet.ORDERING_TOGETHER);
set.setDuration(ACTIVE_ANIMATION_DURATION_MS);
set.setInterpolator(new FastOutSlowInInterpolator());
set.addTransition(new TextScale());
onClickListener =
new OnClickListener() {
@Override
public void onClick(View v) {
final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
MenuItem item = itemView.getItemData();
if (!menu.performItemAction(item, presenter, 0)) {
item.setChecked(true);
}
}
};
tempChildWidths = new int[BottomNavigationMenu.MAX_ITEM_COUNT];
}
@Override
public void initialize(MenuBuilder menu) {
this.menu = menu;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int width = MeasureSpec.getSize(widthMeasureSpec);
// Use visible item count to calculate widths
final int visibleCount = menu.getVisibleItems().size();
// Use total item counts to measure children
final int totalCount = getChildCount();
final int heightSpec = MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY);
if (isShifting(labelVisibilityMode, visibleCount) && itemHorizontalTranslationEnabled) {
final View activeChild = getChildAt(selectedItemPosition);
int activeItemWidth = activeItemMinWidth;
if (activeChild.getVisibility() != View.GONE) {
// Do an AT_MOST measure pass on the active child to get its desired width, and resize the
// active child view based on that width
activeChild.measure(
MeasureSpec.makeMeasureSpec(activeItemMaxWidth, MeasureSpec.AT_MOST), heightSpec);
activeItemWidth = Math.max(activeItemWidth, activeChild.getMeasuredWidth());
}
final int inactiveCount = visibleCount - (activeChild.getVisibility() != View.GONE ? 1 : 0);
final int activeMaxAvailable = width - inactiveCount * inactiveItemMinWidth;
final int activeWidth =
Math.min(activeMaxAvailable, Math.min(activeItemWidth, activeItemMaxWidth));
final int inactiveMaxAvailable =
(width - activeWidth) / (inactiveCount == 0 ? 1 : inactiveCount);
final int inactiveWidth = Math.min(inactiveMaxAvailable, inactiveItemMaxWidth);
int extra = width - activeWidth - inactiveWidth * inactiveCount;
for (int i = 0; i < totalCount; i++) {
if (getChildAt(i).getVisibility() != View.GONE) {
tempChildWidths[i] = (i == selectedItemPosition) ? activeWidth : inactiveWidth;
// Account for integer division which sometimes leaves some extra pixel spaces.
// e.g. If the nav was 10px wide, and 3 children were measured to be 3px-3px-3px, there
// would be a 1px gap somewhere, which this fills in.
if (extra > 0) {
tempChildWidths[i]++;
extra--;
}
} else {
tempChildWidths[i] = 0;
}
}
} else {
final int maxAvailable = width / (visibleCount == 0 ? 1 : visibleCount);
final int childWidth = Math.min(maxAvailable, activeItemMaxWidth);
int extra = width - childWidth * visibleCount;
for (int i = 0; i < totalCount; i++) {
if (getChildAt(i).getVisibility() != View.GONE) {
tempChildWidths[i] = childWidth;
if (extra > 0) {
tempChildWidths[i]++;
extra--;
}
} else {
tempChildWidths[i] = 0;
}
}
}
int totalWidth = 0;
for (int i = 0; i < totalCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
child.measure(
MeasureSpec.makeMeasureSpec(tempChildWidths[i], MeasureSpec.EXACTLY), heightSpec);
ViewGroup.LayoutParams params = child.getLayoutParams();
params.width = child.getMeasuredWidth();
totalWidth += child.getMeasuredWidth();
}
setMeasuredDimension(
View.resolveSizeAndState(
totalWidth, MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
View.resolveSizeAndState(itemHeight, heightSpec, 0));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
final int width = right - left;
final int height = bottom - top;
int used = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
} else {
child.layout(used, 0, child.getMeasuredWidth() + used, height);
}
used += child.getMeasuredWidth();
}
}
@Override
public int getWindowAnimations() {
return 0;
}
/**
* Sets the tint which is applied to the menu items' icons.
*
* @param tint the tint to apply
*/
public void setIconTintList(ColorStateList tint) {
itemIconTint = tint;
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
item.setIconTintList(tint);
}
}
}
/**
* Returns the tint which is applied for the menu item labels.
*
* @return the ColorStateList that is used to tint menu items' icons
*/
@Nullable
public ColorStateList getIconTintList() {
return itemIconTint;
}
/**
* Sets the size to provide for the menu item icons.
*
* <p>For best image resolution, use an icon with the same size set in this method.
*
* @param iconSize the size to provide for the menu item icons in pixels
*/
public void setItemIconSize(@Dimension int iconSize) {
this.itemIconSize = iconSize;
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
item.setIconSize(iconSize);
}
}
}
/** Returns the size in pixels provided for the menu item icons. */
@Dimension
public int getItemIconSize() {
return itemIconSize;
}
/**
* Sets the text color to be used for the menu item labels.
*
* @param color the ColorStateList used for menu item labels
*/
public void setItemTextColor(ColorStateList color) {
itemTextColorFromUser = color;
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
item.setTextColor(color);
}
}
}
/**
* Returns the text color used for menu item labels.
*
* @return the ColorStateList used for menu items labels
*/
public ColorStateList getItemTextColor() {
return itemTextColorFromUser;
}
/**
* Sets the text appearance to be used for inactive menu item labels.
*
* @param textAppearanceRes the text appearance ID used for inactive menu item labels
*/
public void setItemTextAppearanceInactive(@StyleRes int textAppearanceRes) {
this.itemTextAppearanceInactive = textAppearanceRes;
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
item.setTextAppearanceInactive(textAppearanceRes);
// Set the text color if the user has set it, since itemTextColorFromUser takes precedence
// over a color set in the text appearance.
if (itemTextColorFromUser != null) {
item.setTextColor(itemTextColorFromUser);
}
}
}
}
/**
* Returns the text appearance used for inactive menu item labels.
*
* @return the text appearance ID used for inactive menu item labels
*/
@StyleRes
public int getItemTextAppearanceInactive() {
return itemTextAppearanceInactive;
}
/**
* Sets the text appearance to be used for the active menu item label.
*
* @param textAppearanceRes the text appearance ID used for the active menu item label
*/
public void setItemTextAppearanceActive(@StyleRes int textAppearanceRes) {
this.itemTextAppearanceActive = textAppearanceRes;
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
item.setTextAppearanceActive(textAppearanceRes);
// Set the text color if the user has set it, since itemTextColorFromUser takes precedence
// over a color set in the text appearance.
if (itemTextColorFromUser != null) {
item.setTextColor(itemTextColorFromUser);
}
}
}
}
/**
* Returns the text appearance used for the active menu item label.
*
* @return the text appearance ID used for the active menu item label
*/
@StyleRes
public int getItemTextAppearanceActive() {
return itemTextAppearanceActive;
}
/**
* Sets the resource ID to be used for item backgrounds.
*
* @param background the resource ID of the background
*/
public void setItemBackgroundRes(int background) {
itemBackgroundRes = background;
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
item.setItemBackground(background);
}
}
}
/**
* Returns the resource ID for the background of the menu items.
*
* @return the resource ID for the background
*/
public int getItemBackgroundRes() {
return itemBackgroundRes;
}
/**
* Sets the navigation items' label visibility mode.
*
* <p>The label is either always shown, never shown, or only shown when activated. Also supports
* "auto" mode, which uses the item count to determine whether to show or hide the label.
*
* @param labelVisibilityMode mode which decides whether or not the label should be shown. Can be
* one of {@link LabelVisibilityMode#LABEL_VISIBILITY_AUTO}, {@link
* LabelVisibilityMode#LABEL_VISIBILITY_SELECTED}, {@link
* LabelVisibilityMode#LABEL_VISIBILITY_LABELED}, or {@link
* LabelVisibilityMode#LABEL_VISIBILITY_UNLABELED}
* @see #getLabelVisibilityMode()
*/
public void setLabelVisibilityMode(@LabelVisibilityMode int labelVisibilityMode) {
this.labelVisibilityMode = labelVisibilityMode;
}
/**
* Returns the current label visibility mode.
*
* @see #setLabelVisibilityMode(int)
*/
public int getLabelVisibilityMode() {
return labelVisibilityMode;
}
/**
* Sets whether the menu items horizontally translate on selection when the combined item widths
* fill the screen.
*
* @param itemHorizontalTranslationEnabled whether the menu items horizontally translate on
* selection
* @see #isItemHorizontalTranslationEnabled()
*/
public void setItemHorizontalTranslationEnabled(boolean itemHorizontalTranslationEnabled) {
this.itemHorizontalTranslationEnabled = itemHorizontalTranslationEnabled;
}
/**
* Returns whether the menu items horizontally translate on selection when the combined item
* widths fill the screen.
*
* @return whether the menu items horizontally translate on selection
* @see #setItemHorizontalTranslationEnabled(boolean)
*/
public boolean isItemHorizontalTranslationEnabled() {
return itemHorizontalTranslationEnabled;
}
public ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
final TypedValue value = new TypedValue();
if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
return null;
}
ColorStateList baseColor = AppCompatResources.getColorStateList(getContext(), value.resourceId);
if (!getContext()
.getTheme()
.resolveAttribute(android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
return null;
}
int colorPrimary = value.data;
int defaultColor = baseColor.getDefaultColor();
return new ColorStateList(
new int[][] {DISABLED_STATE_SET, CHECKED_STATE_SET, EMPTY_STATE_SET},
new int[] {
baseColor.getColorForState(DISABLED_STATE_SET, defaultColor), colorPrimary, defaultColor
});
}
public void setPresenter(BottomNavigationPresenter presenter) {
this.presenter = presenter;
}
public void buildMenuView() {
removeAllViews();
if (buttons != null) {
for (BottomNavigationItemView item : buttons) {
if (item != null) {
itemPool.release(item);
}
}
}
if (menu.size() == 0) {
selectedItemId = 0;
selectedItemPosition = 0;
buttons = null;
return;
}
buttons = new BottomNavigationItemView[menu.size()];
boolean shifting = isShifting(labelVisibilityMode, menu.getVisibleItems().size());
for (int i = 0; i < menu.size(); i++) {
presenter.setUpdateSuspended(true);
menu.getItem(i).setCheckable(true);
presenter.setUpdateSuspended(false);
BottomNavigationItemView child = getNewItem();
buttons[i] = child;
child.setIconTintList(itemIconTint);
child.setIconSize(itemIconSize);
// Set the text color the default, then look for another text color in order of precedence.
child.setTextColor(itemTextColorDefault);
child.setTextAppearanceInactive(itemTextAppearanceInactive);
child.setTextAppearanceActive(itemTextAppearanceActive);
child.setTextColor(itemTextColorFromUser);
child.setItemBackground(itemBackgroundRes);
child.setShifting(shifting);
child.setLabelVisibilityMode(labelVisibilityMode);
child.initialize((MenuItemImpl) menu.getItem(i), 0);
child.setItemPosition(i);
child.setOnClickListener(onClickListener);
addView(child);
}
selectedItemPosition = Math.min(menu.size() - 1, selectedItemPosition);
menu.getItem(selectedItemPosition).setChecked(true);
}
public void updateMenuView() {
if (menu == null || buttons == null) {
return;
}
final int menuSize = menu.size();
if (menuSize != buttons.length) {
// The size has changed. Rebuild menu view from scratch.
buildMenuView();
return;
}
int previousSelectedId = selectedItemId;
for (int i = 0; i < menuSize; i++) {
MenuItem item = menu.getItem(i);
if (item.isChecked()) {
selectedItemId = item.getItemId();
selectedItemPosition = i;
}
}
if (previousSelectedId != selectedItemId) {
// Note: this has to be called before BottomNavigationItemView#initialize().
TransitionManager.beginDelayedTransition(this, set);
}
boolean shifting = isShifting(labelVisibilityMode, menu.getVisibleItems().size());
for (int i = 0; i < menuSize; i++) {
presenter.setUpdateSuspended(true);
buttons[i].setLabelVisibilityMode(labelVisibilityMode);
buttons[i].setShifting(shifting);
buttons[i].initialize((MenuItemImpl) menu.getItem(i), 0);
presenter.setUpdateSuspended(false);
}
}
private BottomNavigationItemView getNewItem() {
BottomNavigationItemView item = itemPool.acquire();
if (item == null) {
item = new BottomNavigationItemView(getContext());
}
return item;
}
public int getSelectedItemId() {
return selectedItemId;
}
private boolean isShifting(@LabelVisibilityMode int labelVisibilityMode, int childCount) {
return labelVisibilityMode == LabelVisibilityMode.LABEL_VISIBILITY_AUTO
? childCount > 3
: labelVisibilityMode == LabelVisibilityMode.LABEL_VISIBILITY_SELECTED;
}
void tryRestoreSelectedItemId(int itemId) {
final int size = menu.size();
for (int i = 0; i < size; i++) {
MenuItem item = menu.getItem(i);
if (itemId == item.getItemId()) {
selectedItemId = itemId;
selectedItemPosition = i;
item.setChecked(true);
break;
}
}
}
}