/* * Copyright (C) 2020 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.navigation; import com.google.android.material.R; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static java.lang.Math.min; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Rect; import android.graphics.drawable.Drawable; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.menu.MenuBuilder; import androidx.appcompat.view.menu.MenuItemImpl; import androidx.appcompat.view.menu.MenuView; import android.util.SparseArray; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.TextView; import androidx.annotation.Dimension; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.RestrictTo; import androidx.annotation.StyleRes; import androidx.core.util.Pools; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat; import androidx.transition.AutoTransition; import androidx.transition.TransitionManager; import androidx.transition.TransitionSet; import com.google.android.material.animation.AnimationUtils; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.internal.TextScale; import com.google.android.material.motion.MotionUtils; import com.google.android.material.navigation.NavigationBarView.ItemIconGravity; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.ShapeAppearanceModel; import java.util.HashSet; /** * Provides a view that will be use to render a menu view inside a {@link NavigationBarView}. * * @hide */ @RestrictTo(LIBRARY_GROUP) public abstract class NavigationBarMenuView extends ViewGroup implements MenuView { private static final int NO_PADDING = -1; private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; @Nullable private final TransitionSet set; @NonNull private final OnClickListener onClickListener; @Nullable private Pools.Pool itemPool; @NonNull private final SparseArray onTouchListeners = new SparseArray<>(); @NavigationBarView.LabelVisibility private int labelVisibilityMode; @ItemIconGravity private int itemIconGravity; @Nullable private NavigationBarMenuItemView[] buttons; private static final int NO_SELECTED_ITEM = -1; private int selectedItemId = NO_SELECTED_ITEM; private int selectedItemPosition = NO_SELECTED_ITEM; @Nullable private ColorStateList itemIconTint; @Dimension private int itemIconSize; private ColorStateList itemTextColorFromUser; @Nullable private final ColorStateList itemTextColorDefault; @StyleRes private int itemTextAppearanceInactive; @StyleRes private int itemTextAppearanceActive; @StyleRes private int horizontalItemTextAppearanceInactive; @StyleRes private int horizontalItemTextAppearanceActive; private boolean itemTextAppearanceActiveBoldEnabled; private Drawable itemBackground; @Nullable private ColorStateList itemRippleColor; private int itemBackgroundRes; @NonNull private final SparseArray badgeDrawables = new SparseArray<>(); private int itemPaddingTop = NO_PADDING; private int itemPaddingBottom = NO_PADDING; private int itemActiveIndicatorLabelPadding = NO_PADDING; private int iconLabelHorizontalSpacing = NO_PADDING; private boolean itemActiveIndicatorEnabled; private int itemActiveIndicatorWidth; private int itemActiveIndicatorHeight; private int itemActiveIndicatorExpandedWidth; private int itemActiveIndicatorExpandedHeight; private int itemActiveIndicatorMarginHorizontal; private int itemActiveIndicatorExpandedMarginHorizontal; private int itemGravity = NavigationBarView.ITEM_GRAVITY_TOP_CENTER; private ShapeAppearanceModel itemActiveIndicatorShapeAppearance; private boolean itemActiveIndicatorResizeable = false; private ColorStateList itemActiveIndicatorColor; private NavigationBarPresenter presenter; private NavigationBarMenuBuilder menu; private boolean measurePaddingFromLabelBaseline; private boolean scaleLabelWithFont; private int labelMaxLines = 1; private int itemPoolSize = 0; private boolean expanded; private MenuItem checkedItem = null; private static final int DEFAULT_COLLAPSED_MAX_COUNT = 7; private int collapsedMaxItemCount = DEFAULT_COLLAPSED_MAX_COUNT; private boolean dividersEnabled = false; private final Rect itemActiveIndicatorExpandedPadding = new Rect(); public NavigationBarMenuView(@NonNull Context context) { super(context); itemTextColorDefault = createDefaultColorStateList(android.R.attr.textColorSecondary); if (this.isInEditMode()) { set = null; } else { set = new AutoTransition(); set.setOrdering(TransitionSet.ORDERING_TOGETHER); set.excludeTarget(TextView.class, true); set.setDuration( MotionUtils.resolveThemeDuration( getContext(), R.attr.motionDurationMedium4, getResources().getInteger(R.integer.material_motion_duration_long_1))); set.setInterpolator( MotionUtils.resolveThemeInterpolator( getContext(), R.attr.motionEasingStandard, AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)); set.addTransition(new TextScale()); } onClickListener = new OnClickListener() { @Override public void onClick(View v) { final NavigationBarItemView itemView = (NavigationBarItemView) v; MenuItem item = itemView.getItemData(); boolean result = menu.performItemAction(item, presenter, 0); if (item != null && item.isCheckable() && (!result || item.isChecked())) { // If the item action was not invoked successfully (ie if there's no listener) or if // the item was checked through the action, we should update the checked item. setCheckedItem(item); } } }; setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } /** * Set the checked item in the menu view. * * @param checkedItem the item to set checked */ public void setCheckedItem(@NonNull MenuItem checkedItem) { if (this.checkedItem == checkedItem || !checkedItem.isCheckable()) { return; } // Unset the previous checked item if (this.checkedItem != null && this.checkedItem.isChecked()) { this.checkedItem.setChecked(false); } checkedItem.setChecked(true); this.checkedItem = checkedItem; } /** Set the current expanded state. */ public void setExpanded(boolean expanded) { this.expanded = expanded; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { item.setExpanded(expanded); } } } /** Returns the current expanded state. */ public boolean isExpanded() { return expanded; } @Override public void initialize(@NonNull MenuBuilder menu) { this.menu = new NavigationBarMenuBuilder(menu); } @Override public int getWindowAnimations() { return 0; } @Override public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info); infoCompat.setCollectionInfo( CollectionInfoCompat.obtain( /* rowCount= */ 1, /* columnCount= */ getCurrentVisibleContentItemCount(), /* hierarchical= */ false, /* selectionMode= */ CollectionInfoCompat.SELECTION_MODE_SINGLE)); } /** * Sets the tint which is applied to the menu items' icons. * * @param tint the tint to apply */ public void setIconTintList(@Nullable ColorStateList tint) { itemIconTint = tint; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) 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. * *

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 (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) 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(@Nullable ColorStateList color) { itemTextColorFromUser = color; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setTextColor(color); } } } } /** * Returns the text color used for menu item labels. * * @return the ColorStateList used for menu items labels */ @Nullable 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 (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setTextAppearanceInactive(textAppearanceRes); } } } } /** * 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 (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setTextAppearanceActive(textAppearanceRes); } } } } /** * Sets whether the active menu item label is bold. * * @param isBold whether the active menu item label is bold */ public void setItemTextAppearanceActiveBoldEnabled(boolean isBold) { this.itemTextAppearanceActiveBoldEnabled = isBold; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setTextAppearanceActiveBoldEnabled(isBold); } } } } /** * 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 text appearance to be used for inactive menu item labels when they are in the * horizontal item layout (when the start icon value is {@link * ItemIconGravity#ITEM_ICON_GRAVITY_START}). * * @param textAppearanceRes the text appearance ID used for inactive menu item labels */ public void setHorizontalItemTextAppearanceInactive(@StyleRes int textAppearanceRes) { this.horizontalItemTextAppearanceInactive = textAppearanceRes; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setHorizontalTextAppearanceInactive(textAppearanceRes); } } } } /** * Returns the text appearance used for inactive menu item labels when they are in the horizontal * item layout (when the start icon value is {@link ItemIconGravity#ITEM_ICON_GRAVITY_START}). * * @return the text appearance ID used for inactive menu item labels */ @StyleRes public int getHorizontalItemTextAppearanceInactive() { return horizontalItemTextAppearanceInactive; } /** * Sets the text appearance to be used for the active menu item label when they are in the * horizontal item layout (when the start icon value is {@link * ItemIconGravity#ITEM_ICON_GRAVITY_START}). * * @param textAppearanceRes the text appearance ID used for the active menu item label */ public void setHorizontalItemTextAppearanceActive(@StyleRes int textAppearanceRes) { this.horizontalItemTextAppearanceActive = textAppearanceRes; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setHorizontalTextAppearanceActive(textAppearanceRes); } } } } /** * Returns the text appearance used for the active menu item label when they are in the horizontal * item layout (when the start icon value is {@link ItemIconGravity#ITEM_ICON_GRAVITY_START}). * * @return the text appearance ID used for the active menu item label */ @StyleRes public int getHorizontalItemTextAppearanceActive() { return horizontalItemTextAppearanceActive; } /** * 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 (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemBackground(background); } } } } /** * Get the distance from the top of an item's icon/active indicator to the top of the navigation * bar item. */ @Px public int getItemPaddingTop() { return itemPaddingTop; } /** * Set the distance from the top of an items icon/active indicator to the top of the navigation * bar item. */ public void setItemPaddingTop(@Px int paddingTop) { itemPaddingTop = paddingTop; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemPaddingTop(paddingTop); } } } } /** * Get the distance from the bottom of an item's label to the bottom of the navigation bar item. */ @Px public int getItemPaddingBottom() { return itemPaddingBottom; } /** * Set the distance from the bottom of an item's label to the bottom of the navigation bar item. */ public void setItemPaddingBottom(@Px int paddingBottom) { itemPaddingBottom = paddingBottom; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemPaddingBottom(itemPaddingBottom); } } } } public void setMeasurePaddingFromLabelBaseline(boolean measurePaddingFromLabelBaseline) { this.measurePaddingFromLabelBaseline = measurePaddingFromLabelBaseline; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setMeasureBottomPaddingFromLabelBaseline(measurePaddingFromLabelBaseline); } } } } public void setLabelFontScalingEnabled(boolean scaleLabelWithFont) { this.scaleLabelWithFont = scaleLabelWithFont; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setLabelFontScalingEnabled(scaleLabelWithFont); } } } } public boolean getScaleLabelTextWithFont() { return scaleLabelWithFont; } public void setLabelMaxLines(int labelMaxLines) { this.labelMaxLines = labelMaxLines; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setLabelMaxLines(labelMaxLines); } } } } public int getLabelMaxLines() { return labelMaxLines; } /** * Get the distance between the item's active indicator container and the label. */ @Px public int getActiveIndicatorLabelPadding() { return itemActiveIndicatorLabelPadding; } /** * Set the distance between the active indicator container and the item's label. */ public void setActiveIndicatorLabelPadding(@Px int activeIndicatorLabelPadding) { itemActiveIndicatorLabelPadding = activeIndicatorLabelPadding; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setActiveIndicatorLabelPadding(activeIndicatorLabelPadding); } } } } /** * Get the horizontal distance between the item's icon and the label which * is shown when the item is in the {@link NavigationBarView#ITEM_ICON_GRAVITY_START} * configuration. */ @Px public int getIconLabelHorizontalSpacing() { return iconLabelHorizontalSpacing; } /** * Set the horizontal distance between the icon and the label which is shown when the item is in * the {@link NavigationBarView#ITEM_ICON_GRAVITY_START} configuration. */ public void setIconLabelHorizontalSpacing( @Px int iconLabelHorizontalSpacing) { this.iconLabelHorizontalSpacing = iconLabelHorizontalSpacing; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setIconLabelHorizontalSpacing(iconLabelHorizontalSpacing); } } } } /** * Returns whether or not an active indicator is enabled for the navigation bar. * * @return true if the active indicator is enabled. */ public boolean getItemActiveIndicatorEnabled() { return itemActiveIndicatorEnabled; } /** * Set whether or not an active indicator is enabled for the navigation bar. * * @param enabled true if an active indicator should be shown. */ public void setItemActiveIndicatorEnabled(boolean enabled) { this.itemActiveIndicatorEnabled = enabled; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorEnabled(enabled); } } } } /** * Get the width of the selected item's active indicator. * * @return The width, in pixels, of the active indicator. */ @Px public int getItemActiveIndicatorWidth() { return itemActiveIndicatorWidth; } /** * Set the width to be used for the selected item's active indicator. * * @param width The width, in pixels, of the active indicator. */ public void setItemActiveIndicatorWidth(@Px int width) { this.itemActiveIndicatorWidth = width; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorWidth(width); } } } } /** * Get the height of the selected item's active indicator. * * @return The height, in pixels, of the active indicator. */ @Px public int getItemActiveIndicatorHeight() { return itemActiveIndicatorHeight; } /** * Set the height to be used for the selected item's active indicator. * * @param height The height, in pixels, of the active indicator. */ public void setItemActiveIndicatorHeight(@Px int height) { this.itemActiveIndicatorHeight = height; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorHeight(height); } } } } /** * Sets the navigation items' layout gravity. * * @param itemGravity the layout {@link android.view.Gravity} of the item * @see #getItemGravity() */ public void setItemGravity(int itemGravity) { this.itemGravity = itemGravity; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemGravity(itemGravity); } } } } /** * Returns the navigation items' layout gravity. * * @see #setItemGravity(int) */ public int getItemGravity() { return itemGravity; } /** * Get the width of the selected item's active indicator when expanded. * * @return The width, in pixels, of the active indicator when expanded. */ @Px public int getItemActiveIndicatorExpandedWidth() { return itemActiveIndicatorExpandedWidth; } /** * Set the width to be used for the selected item's active indicator when it is expanded, ie. set * to an item gravity of {@link ItemIconGravity#ITEM_ICON_GRAVITY_START}. * * @param width The width, in pixels, of the menu item's expanded active indicator. The width may * also be set as {@link NavigationBarView#ACTIVE_INDICATOR_WIDTH_WRAP_CONTENT} or * {@link NavigationBarView#ACTIVE_INDICATOR_WIDTH_MATCH_PARENT}. */ public void setItemActiveIndicatorExpandedWidth(@Px int width) { this.itemActiveIndicatorExpandedWidth = width; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorExpandedWidth(width); } } } } /** * Get the height of the selected item's active indicator when expanded. * * @return The height, in pixels, of the active indicator when expanded. */ @Px public int getItemActiveIndicatorExpandedHeight() { return itemActiveIndicatorExpandedHeight; } /** * Set the height to be used for the selected item's active indicator when set to an item gravity * of {@link ItemIconGravity#ITEM_ICON_GRAVITY_START}. * * @param height The height, in pixels, of the active indicator. */ public void setItemActiveIndicatorExpandedHeight(@Px int height) { this.itemActiveIndicatorExpandedHeight = height; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorExpandedHeight(height); } } } } /** * Get the margin that will be maintained at the start and end of the active indicator away from * the edges of its parent container. * * @return The horizontal margin, in pixels. */ @Px public int getItemActiveIndicatorMarginHorizontal() { return itemActiveIndicatorMarginHorizontal; } /** * Set the horizontal margin that will be maintained at the start and end of the active indicator, * making sure the indicator remains the given distance from the edge of its parent container. * * @param marginHorizontal The horizontal margin, in pixels. */ public void setItemActiveIndicatorMarginHorizontal(@Px int marginHorizontal) { itemActiveIndicatorMarginHorizontal = marginHorizontal; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorMarginHorizontal(marginHorizontal); } } } } /** * Get the margin that will be maintained at the start and end of the expanded active indicator * away from the edges of its parent container. * * @return The horizontal margin, in pixels. */ @Px public int getItemActiveIndicatorExpandedMarginHorizontal() { return itemActiveIndicatorExpandedMarginHorizontal; } /** * Set the horizontal margin that will be maintained at the start and end of the expanded active * indicator, making sure the indicator remains the given distance from the edge of its parent * container. * * @param marginHorizontal The horizontal margin, in pixels. */ public void setItemActiveIndicatorExpandedMarginHorizontal(@Px int marginHorizontal) { itemActiveIndicatorExpandedMarginHorizontal = marginHorizontal; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setActiveIndicatorExpandedMarginHorizontal(marginHorizontal); } } } } /** * Set the padding of the expanded active indicator wrapping the content. * * @param paddingLeft The left padding, in pixels. * @param paddingTop The top padding, in pixels. * @param paddingRight The right padding, in pixels. * @param paddingBottom The bottom padding, in pixels. */ public void setItemActiveIndicatorExpandedPadding(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { itemActiveIndicatorExpandedPadding.left = paddingLeft; itemActiveIndicatorExpandedPadding.top = paddingTop; itemActiveIndicatorExpandedPadding.right = paddingRight; itemActiveIndicatorExpandedPadding.bottom = paddingBottom; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setActiveIndicatorExpandedPadding(itemActiveIndicatorExpandedPadding); } } } } /** * Get the {@link ShapeAppearanceModel} of the active indicator drawable. * * @return The {@link ShapeAppearanceModel} of the active indicator drawable. */ @Nullable public ShapeAppearanceModel getItemActiveIndicatorShapeAppearance() { return itemActiveIndicatorShapeAppearance; } /** * Set the {@link ShapeAppearanceModel} of the active indicator drawable. * * @param shapeAppearance The {@link ShapeAppearanceModel} of the active indicator drawable. */ public void setItemActiveIndicatorShapeAppearance( @Nullable ShapeAppearanceModel shapeAppearance) { this.itemActiveIndicatorShapeAppearance = shapeAppearance; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setActiveIndicatorDrawable(createItemActiveIndicatorDrawable()); } } } } /** * Get whether the active indicator can be resized. */ protected boolean isItemActiveIndicatorResizeable() { return this.itemActiveIndicatorResizeable; } /** * Set whether the active indicator can be resized. If true, the indicator will automatically * change size in response to label visibility modes. */ protected void setItemActiveIndicatorResizeable(boolean resizeable) { this.itemActiveIndicatorResizeable = resizeable; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setActiveIndicatorResizeable(resizeable); } } } } /** * Get the color of the active indicator drawable. * * @return A {@link ColorStateList} used as the color of the active indicator. */ @Nullable public ColorStateList getItemActiveIndicatorColor() { return itemActiveIndicatorColor; } /** * Set the {@link ColorStateList} of the active indicator drawable. * * @param csl The {@link ColorStateList} used as the color of the active indicator. */ public void setItemActiveIndicatorColor(@Nullable ColorStateList csl) { this.itemActiveIndicatorColor = csl; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item) .setActiveIndicatorDrawable(createItemActiveIndicatorDrawable()); } } } } /** * Create a drawable using the {@code itemActiveIndicatorShapeAppearance} and {@code * itemActiveIndicatorColor} to be used as an item's active indicator. * *

This method is called once per menu item so each item has a unique drawable instance which * can be manipulated/animated independently. * * @return A drawable to be used as a menu item's active indicator. */ @Nullable private Drawable createItemActiveIndicatorDrawable() { if (itemActiveIndicatorShapeAppearance != null && itemActiveIndicatorColor != null) { MaterialShapeDrawable drawable = new MaterialShapeDrawable(itemActiveIndicatorShapeAppearance); drawable.setFillColor(itemActiveIndicatorColor); return drawable; } return null; } /** * Returns the resource ID for the background of the menu items. * * @return the resource ID for the background * @deprecated Use {@link #getItemBackground()} instead. */ @Deprecated public int getItemBackgroundRes() { return itemBackgroundRes; } /** * Sets the drawable to be used for item backgrounds. * * @param background the drawable of the background */ public void setItemBackground(@Nullable Drawable background) { itemBackground = background; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemBackground(background); } } } } /** * Sets the color of the item's ripple. * * This will only be used if there is not a custom background set on the item. * * @param itemRippleColor the color of the ripple */ public void setItemRippleColor(@Nullable ColorStateList itemRippleColor) { this.itemRippleColor = itemRippleColor; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemRippleColor(itemRippleColor); } } } } /** * Returns the color to be used for the items ripple. * * @return the color for the items ripple */ @Nullable public ColorStateList getItemRippleColor() { return itemRippleColor; } /** * Returns the drawable for the background of the menu items. * * @return the drawable for the background */ @Nullable public Drawable getItemBackground() { if (buttons != null && buttons.length > 0) { // Find the first instance of NavigationBarItemView for (NavigationBarMenuItemView button : buttons) { if (button instanceof NavigationBarItemView) { // Return button background instead of itemBackground if possible, so that the correct // drawable is returned if the background is set via #setItemBackgroundRes. return ((NavigationBarItemView) button).getBackground(); } } } return itemBackground; } /** * Sets the navigation items' label visibility mode. * *

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 NavigationBarView#LABEL_VISIBILITY_AUTO}, {@link * NavigationBarView#LABEL_VISIBILITY_SELECTED}, {@link * NavigationBarView#LABEL_VISIBILITY_LABELED}, or {@link * NavigationBarView#LABEL_VISIBILITY_UNLABELED} * @see #getLabelVisibilityMode() */ public void setLabelVisibilityMode(@NavigationBarView.LabelVisibility int labelVisibilityMode) { this.labelVisibilityMode = labelVisibilityMode; } /** * Returns the current label visibility mode. * * @see #setLabelVisibilityMode(int) */ public int getLabelVisibilityMode() { return labelVisibilityMode; } /** * Sets the navigation items' icon gravity. * * @param itemIconGravity the placement of the icon in the nav item one of {@link * NavigationBarView#ITEM_ICON_GRAVITY_TOP}, or {@link * NavigationBarView#ITEM_ICON_GRAVITY_START} * @see #getItemIconGravity() */ public void setItemIconGravity(@ItemIconGravity int itemIconGravity) { this.itemIconGravity = itemIconGravity; if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).setItemIconGravity(itemIconGravity); } } } } /** * Returns the current item icon gravity. * * @see #setItemIconGravity(int) */ @ItemIconGravity public int getItemIconGravity() { return itemIconGravity; } /** * Sets an {@link android.view.View.OnTouchListener} for the item view associated with the * provided {@code menuItemId}. */ @SuppressLint("ClickableViewAccessibility") public void setItemOnTouchListener(int menuItemId, @Nullable OnTouchListener onTouchListener) { if (onTouchListener == null) { onTouchListeners.remove(menuItemId); } else { onTouchListeners.put(menuItemId, onTouchListener); } if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView && item.getItemData() != null && item.getItemData().getItemId() == menuItemId) { ((NavigationBarItemView) item).setOnTouchListener(onTouchListener); } } } } @Nullable 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(androidx.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(@NonNull NavigationBarPresenter presenter) { this.presenter = presenter; } private void releaseItemPool() { if (buttons != null && itemPool != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { itemPool.release((NavigationBarItemView) item); ((NavigationBarItemView) item).clear(); } } } } private NavigationBarItemView createMenuItem( int index, MenuItemImpl item, boolean shifting, boolean hideWhenCollapsed) { presenter.setUpdateSuspended(true); item.setCheckable(true); presenter.setUpdateSuspended(false); NavigationBarItemView child = getNewItem(); child.setShifting(shifting); child.setLabelMaxLines(labelMaxLines); 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.setHorizontalTextAppearanceInactive(horizontalItemTextAppearanceInactive); child.setHorizontalTextAppearanceActive(horizontalItemTextAppearanceActive); child.setTextAppearanceActiveBoldEnabled(itemTextAppearanceActiveBoldEnabled); child.setTextColor(itemTextColorFromUser); if (itemPaddingTop != NO_PADDING) { child.setItemPaddingTop(itemPaddingTop); } if (itemPaddingBottom != NO_PADDING) { child.setItemPaddingBottom(itemPaddingBottom); } child.setMeasureBottomPaddingFromLabelBaseline(measurePaddingFromLabelBaseline); child.setLabelFontScalingEnabled(scaleLabelWithFont); if (itemActiveIndicatorLabelPadding != NO_PADDING) { child.setActiveIndicatorLabelPadding(itemActiveIndicatorLabelPadding); } if (iconLabelHorizontalSpacing != NO_PADDING) { child.setIconLabelHorizontalSpacing(iconLabelHorizontalSpacing); } child.setActiveIndicatorWidth(itemActiveIndicatorWidth); child.setActiveIndicatorHeight(itemActiveIndicatorHeight); child.setActiveIndicatorExpandedWidth(itemActiveIndicatorExpandedWidth); child.setActiveIndicatorExpandedHeight(itemActiveIndicatorExpandedHeight); child.setActiveIndicatorMarginHorizontal(itemActiveIndicatorMarginHorizontal); child.setItemGravity(itemGravity); child.setActiveIndicatorExpandedPadding(itemActiveIndicatorExpandedPadding); child.setActiveIndicatorExpandedMarginHorizontal(itemActiveIndicatorExpandedMarginHorizontal); child.setActiveIndicatorDrawable(createItemActiveIndicatorDrawable()); child.setActiveIndicatorResizeable(itemActiveIndicatorResizeable); child.setActiveIndicatorEnabled(itemActiveIndicatorEnabled); if (itemBackground != null) { child.setItemBackground(itemBackground); } else { child.setItemBackground(itemBackgroundRes); } child.setItemRippleColor(itemRippleColor); child.setLabelVisibilityMode(labelVisibilityMode); child.setItemIconGravity(itemIconGravity); child.setOnlyShowWhenExpanded(hideWhenCollapsed); child.setExpanded(expanded); child.initialize(item, 0); child.setItemPosition(index); int itemId = item.getItemId(); child.setOnTouchListener(onTouchListeners.get(itemId)); child.setOnClickListener(onClickListener); if (selectedItemId != Menu.NONE && itemId == selectedItemId) { selectedItemPosition = index; } setBadgeIfNeeded(child); return child; } @SuppressLint("ClickableViewAccessibility") public void buildMenuView() { removeAllViews(); releaseItemPool(); presenter.setUpdateSuspended(true); menu.refreshItems(); presenter.setUpdateSuspended(false); int contentItemCount = menu.getContentItemCount(); if (contentItemCount == 0) { selectedItemId = 0; selectedItemPosition = 0; buttons = null; itemPool = null; return; } if (itemPool == null || itemPoolSize != contentItemCount) { itemPoolSize = contentItemCount; itemPool = new Pools.SynchronizedPool<>(contentItemCount); } removeUnusedBadges(); int menuSize = menu.size(); buttons = new NavigationBarMenuItemView[menuSize]; int collapsedItemsSoFar = 0; int nextSubheaderItemCount = 0; boolean shifting = isShifting(labelVisibilityMode, getCurrentVisibleContentItemCount()); for (int i = 0; i < menuSize; i++) { MenuItem menuItem = menu.getItemAt(i); NavigationBarMenuItemView child; if (menuItem instanceof DividerMenuItem) { // Add a divider child = new NavigationBarDividerView(getContext()); child.setOnlyShowWhenExpanded(true); ((NavigationBarDividerView) child).setDividersEnabled(dividersEnabled); } else if (menuItem.hasSubMenu()) { if (nextSubheaderItemCount > 0) { // We do not support submenus inside submenus. If there is still subheader items to be // instantiated, we should not have another submenu. throw new IllegalArgumentException( "Only one layer of submenu is supported; a submenu " + "inside a submenu is not supported by the Navigation Bar."); } // Add subheader item child = new NavigationBarSubheaderView(getContext()); ((NavigationBarSubheaderView) child). setTextAppearance(horizontalItemTextAppearanceActive != 0 ? horizontalItemTextAppearanceActive : itemTextAppearanceActive); ((NavigationBarSubheaderView) child).setTextColor(itemTextColorFromUser); child.setOnlyShowWhenExpanded(true); child.initialize((MenuItemImpl) menuItem, 0); nextSubheaderItemCount = menuItem.getSubMenu().size(); } else if (nextSubheaderItemCount > 0) { // Add submenu items child = createMenuItem(i, (MenuItemImpl) menuItem, shifting, /* hideWhenCollapsed= */ true); nextSubheaderItemCount--; } else { child = createMenuItem( i, (MenuItemImpl) menuItem, shifting, collapsedItemsSoFar >= collapsedMaxItemCount); collapsedItemsSoFar++; } if (!(menuItem instanceof DividerMenuItem) && menuItem.isCheckable() && selectedItemPosition == NO_SELECTED_ITEM) { selectedItemPosition = i; } buttons[i] = child; addView((View) child); } selectedItemPosition = min(menuSize - 1, selectedItemPosition); setCheckedItem(buttons[selectedItemPosition].getItemData()); } private boolean isMenuStructureSame() { if (buttons == null || menu == null || menu.size() != buttons.length) { return false; } for (int i = 0; i < buttons.length; i++) { // If the menu item is a divider but the existing item is not a divider, return false if (menu.getItemAt(i) instanceof DividerMenuItem && !(buttons[i] instanceof NavigationBarDividerView)) { return false; } boolean incorrectSubheaderType = menu.getItemAt(i).hasSubMenu() && !(buttons[i] instanceof NavigationBarSubheaderView); boolean incorrectItemType = !menu.getItemAt(i).hasSubMenu() && !(buttons[i] instanceof NavigationBarItemView); if (!(menu.getItemAt(i) instanceof DividerMenuItem) && (incorrectSubheaderType || incorrectItemType)) { return false; } } return true; } public void updateMenuView() { if (menu == null || buttons == null) { return; } presenter.setUpdateSuspended(true); menu.refreshItems(); presenter.setUpdateSuspended(false); if (!isMenuStructureSame()) { buildMenuView(); return; } int previousSelectedId = selectedItemId; int menuSize = menu.size(); for (int i = 0; i < menuSize; i++) { MenuItem item = menu.getItemAt(i); if (item.isChecked()) { setCheckedItem(item); selectedItemId = item.getItemId(); selectedItemPosition = i; } } if (previousSelectedId != selectedItemId && set != null) { // Note: this has to be called before NavigationBarItemView#initialize(). TransitionManager.beginDelayedTransition(this, set); } boolean shifting = isShifting(labelVisibilityMode, getCurrentVisibleContentItemCount()); for (int i = 0; i < menuSize; i++) { presenter.setUpdateSuspended(true); buttons[i].setExpanded(expanded); if (buttons[i] instanceof NavigationBarItemView) { NavigationBarItemView itemView = (NavigationBarItemView) buttons[i]; itemView.setLabelVisibilityMode(labelVisibilityMode); itemView.setItemIconGravity(itemIconGravity); itemView.setItemGravity(itemGravity); itemView.setShifting(shifting); } if (menu.getItemAt(i) instanceof MenuItemImpl) { buttons[i].initialize((MenuItemImpl) menu.getItemAt(i), 0); } presenter.setUpdateSuspended(false); } } private NavigationBarItemView getNewItem() { NavigationBarItemView item = itemPool != null ? itemPool.acquire() : null; if (item == null) { item = createNavigationBarItemView(getContext()); } return item; } public void setSubmenuDividersEnabled(boolean dividersEnabled) { if (this.dividersEnabled == dividersEnabled) { return; } this.dividersEnabled = dividersEnabled; if (buttons != null) { for (NavigationBarMenuItemView itemView : buttons) { if (itemView instanceof NavigationBarDividerView) { ((NavigationBarDividerView) itemView).setDividersEnabled(dividersEnabled); } } } } public void setCollapsedMaxItemCount(int collapsedMaxCount) { this.collapsedMaxItemCount = collapsedMaxCount; } private int getCollapsedVisibleItemCount() { return min(collapsedMaxItemCount, menu.getVisibleMainContentItemCount()); } public int getCurrentVisibleContentItemCount() { return expanded ? menu.getVisibleContentItemCount() : getCollapsedVisibleItemCount(); } public int getSelectedItemId() { return selectedItemId; } protected boolean isShifting( @NavigationBarView.LabelVisibility int labelVisibilityMode, int childCount) { return labelVisibilityMode == NavigationBarView.LABEL_VISIBILITY_AUTO ? childCount > 3 : labelVisibilityMode == NavigationBarView.LABEL_VISIBILITY_SELECTED; } void tryRestoreSelectedItemId(int itemId) { final int size = menu.size(); for (int i = 0; i < size; i++) { MenuItem item = menu.getItemAt(i); if (itemId == item.getItemId()) { selectedItemId = itemId; selectedItemPosition = i; setCheckedItem(item); break; } } } SparseArray getBadgeDrawables() { return badgeDrawables; } void restoreBadgeDrawables(SparseArray badgeDrawables) { for (int i = 0; i < badgeDrawables.size(); i++) { int key = badgeDrawables.keyAt(i); if (this.badgeDrawables.indexOfKey(key) < 0) { // badge doesn't exist yet, restore it this.badgeDrawables.append(key, badgeDrawables.get(key)); } } if (buttons != null) { for (NavigationBarMenuItemView itemView : buttons) { if (itemView instanceof NavigationBarItemView) { BadgeDrawable badge = this.badgeDrawables.get(((NavigationBarItemView) itemView).getId()); if (badge != null) { ((NavigationBarItemView) itemView).setBadge(badge); } } } } } @Nullable public BadgeDrawable getBadge(int menuItemId) { return badgeDrawables.get(menuItemId); } /** * Creates an instance of {@link BadgeDrawable} if none exists. Initializes (if needed) and * returns the associated instance of {@link BadgeDrawable}. * * @param menuItemId Id of the menu item. * @return an instance of BadgeDrawable associated with {@code menuItemId}. */ BadgeDrawable getOrCreateBadge(int menuItemId) { validateMenuItemId(menuItemId); BadgeDrawable badgeDrawable = badgeDrawables.get(menuItemId); // Create an instance of BadgeDrawable if none were already initialized for this menu item. if (badgeDrawable == null) { badgeDrawable = BadgeDrawable.create(getContext()); badgeDrawables.put(menuItemId, badgeDrawable); } NavigationBarItemView itemView = findItemView(menuItemId); if (itemView != null) { itemView.setBadge(badgeDrawable); } return badgeDrawable; } void removeBadge(int menuItemId) { validateMenuItemId(menuItemId); NavigationBarItemView itemView = findItemView(menuItemId); if (itemView != null) { itemView.removeBadge(); } badgeDrawables.put(menuItemId, null); } private void setBadgeIfNeeded(@NonNull NavigationBarItemView child) { int childId = child.getId(); if (!isValidId(childId)) { // Child doesn't have a valid id, do not set any BadgeDrawable on the view. return; } BadgeDrawable badgeDrawable = badgeDrawables.get(childId); if (badgeDrawable != null) { child.setBadge(badgeDrawable); } } private void removeUnusedBadges() { HashSet activeKeys = new HashSet<>(); // Remove keys from badgeDrawables that don't have a corresponding value in the menu. for (int i = 0; i < menu.size(); i++) { activeKeys.add(menu.getItemAt(i).getItemId()); } for (int i = 0; i < badgeDrawables.size(); i++) { int key = badgeDrawables.keyAt(i); if (!activeKeys.contains(key)) { badgeDrawables.delete(key); } } } @Nullable public NavigationBarItemView findItemView(int menuItemId) { validateMenuItemId(menuItemId); if (buttons != null) { for (NavigationBarMenuItemView itemView : buttons) { if (itemView instanceof NavigationBarItemView) { if (((NavigationBarItemView) itemView).getId() == menuItemId) { return (NavigationBarItemView) itemView; } } } } return null; } /** Returns reference to newly created {@link NavigationBarItemView}. */ @NonNull protected abstract NavigationBarItemView createNavigationBarItemView(@NonNull Context context); protected int getSelectedItemPosition() { return selectedItemPosition; } @Nullable protected NavigationBarMenuBuilder getMenu() { return menu; } private boolean isValidId(int viewId) { return viewId != View.NO_ID; } private void validateMenuItemId(int viewId) { if (!isValidId(viewId)) { throw new IllegalArgumentException(viewId + " is not a valid view id"); } } public void updateActiveIndicator(int availableWidth) { if (buttons != null) { for (NavigationBarMenuItemView item : buttons) { if (item instanceof NavigationBarItemView) { ((NavigationBarItemView) item).updateActiveIndicatorLayoutParams(availableWidth); } } } } }