mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-21 04:22:42 +08:00
Resolves https://github.com/material-components/material-components-android/pull/2654 GIT_ORIGIN_REV_ID=b8f6728979875629f2c813ef90d2f671cf56b4eb PiperOrigin-RevId: 442852630
1002 lines
36 KiB
Java
1002 lines
36 KiB
Java
/*
|
|
* 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.max;
|
|
import static java.lang.Math.min;
|
|
|
|
import android.animation.ValueAnimator;
|
|
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
|
import android.content.Context;
|
|
import android.content.res.ColorStateList;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Build.VERSION;
|
|
import android.os.Build.VERSION_CODES;
|
|
import androidx.appcompat.view.menu.MenuItemImpl;
|
|
import androidx.appcompat.view.menu.MenuView;
|
|
import androidx.appcompat.widget.TooltipCompat;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.util.TypedValue;
|
|
import android.view.Gravity;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
import androidx.annotation.DimenRes;
|
|
import androidx.annotation.DrawableRes;
|
|
import androidx.annotation.FloatRange;
|
|
import androidx.annotation.LayoutRes;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.Px;
|
|
import androidx.annotation.RestrictTo;
|
|
import androidx.annotation.StyleRes;
|
|
import androidx.core.content.ContextCompat;
|
|
import androidx.core.graphics.drawable.DrawableCompat;
|
|
import androidx.core.view.PointerIconCompat;
|
|
import androidx.core.view.ViewCompat;
|
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
|
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
|
|
import androidx.core.widget.TextViewCompat;
|
|
import com.google.android.material.animation.AnimationUtils;
|
|
import com.google.android.material.badge.BadgeDrawable;
|
|
import com.google.android.material.badge.BadgeUtils;
|
|
import com.google.android.material.motion.MotionUtils;
|
|
import com.google.android.material.resources.MaterialResources;
|
|
|
|
/**
|
|
* Provides a view that will be used to render destination items inside a {@link
|
|
* NavigationBarMenuView}.
|
|
*
|
|
* @hide
|
|
*/
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
public abstract class NavigationBarItemView extends FrameLayout implements MenuView.ItemView {
|
|
private static final int INVALID_ITEM_POSITION = -1;
|
|
private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
|
|
|
|
private boolean initialized = false;
|
|
private int itemPaddingTop;
|
|
private int itemPaddingBottom;
|
|
private float shiftAmount;
|
|
private float scaleUpFactor;
|
|
private float scaleDownFactor;
|
|
|
|
private int labelVisibilityMode;
|
|
private boolean isShifting;
|
|
|
|
@Nullable private final FrameLayout iconContainer;
|
|
@Nullable private final View activeIndicatorView;
|
|
private final ImageView icon;
|
|
private final ViewGroup labelGroup;
|
|
private final TextView smallLabel;
|
|
private final TextView largeLabel;
|
|
private int itemPosition = INVALID_ITEM_POSITION;
|
|
|
|
@Nullable private MenuItemImpl itemData;
|
|
|
|
@Nullable private ColorStateList iconTint;
|
|
@Nullable private Drawable originalIconDrawable;
|
|
@Nullable private Drawable wrappedIconDrawable;
|
|
|
|
private static final ActiveIndicatorTransform ACTIVE_INDICATOR_LABELED_TRANSFORM =
|
|
new ActiveIndicatorTransform();
|
|
private static final ActiveIndicatorTransform ACTIVE_INDICATOR_UNLABELED_TRANSFORM =
|
|
new ActiveIndicatorUnlabeledTransform();
|
|
|
|
private ValueAnimator activeIndicatorAnimator;
|
|
private ActiveIndicatorTransform activeIndicatorTransform = ACTIVE_INDICATOR_LABELED_TRANSFORM;
|
|
private float activeIndicatorProgress = 0F;
|
|
private boolean activeIndicatorEnabled = false;
|
|
// The desired width of the indicator. This is not necessarily the actual size of the rendered
|
|
// indicator depending on whether the width of this view is wide enough to accommodate the full
|
|
// desired width.
|
|
private int activeIndicatorDesiredWidth = 0;
|
|
private int activeIndicatorDesiredHeight = 0;
|
|
private boolean activeIndicatorResizeable = false;
|
|
// The margin from the start and end of this view which the active indicator should respect. If
|
|
// the indicator width is greater than the total width minus the horizontal margins, the active
|
|
// indicator will assume the max width of the view's total width minus horizontal margins.
|
|
private int activeIndicatorMarginHorizontal = 0;
|
|
|
|
@Nullable private BadgeDrawable badgeDrawable;
|
|
|
|
public NavigationBarItemView(@NonNull Context context) {
|
|
super(context);
|
|
|
|
LayoutInflater.from(context).inflate(getItemLayoutResId(), this, true);
|
|
iconContainer = findViewById(R.id.navigation_bar_item_icon_container);
|
|
activeIndicatorView = findViewById(R.id.navigation_bar_item_active_indicator_view);
|
|
icon = findViewById(R.id.navigation_bar_item_icon_view);
|
|
labelGroup = findViewById(R.id.navigation_bar_item_labels_group);
|
|
smallLabel = findViewById(R.id.navigation_bar_item_small_label_view);
|
|
largeLabel = findViewById(R.id.navigation_bar_item_large_label_view);
|
|
|
|
setBackgroundResource(getItemBackgroundResId());
|
|
|
|
itemPaddingTop = getResources().getDimensionPixelSize(getItemDefaultMarginResId());
|
|
itemPaddingBottom = labelGroup.getPaddingBottom();
|
|
|
|
// The labels used aren't always visible, so they are unreliable for accessibility. Instead,
|
|
// the content description of the NavigationBarItemView should be used for accessibility.
|
|
ViewCompat.setImportantForAccessibility(smallLabel, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
ViewCompat.setImportantForAccessibility(largeLabel, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
setFocusable(true);
|
|
calculateTextScaleFactors(smallLabel.getTextSize(), largeLabel.getTextSize());
|
|
|
|
// TODO(b/138148581): Support displaying a badge on label-only bottom navigation views.
|
|
if (icon != null) {
|
|
icon.addOnLayoutChangeListener(
|
|
new OnLayoutChangeListener() {
|
|
@Override
|
|
public void onLayoutChange(
|
|
View v,
|
|
int left,
|
|
int top,
|
|
int right,
|
|
int bottom,
|
|
int oldLeft,
|
|
int oldTop,
|
|
int oldRight,
|
|
int oldBottom) {
|
|
if (icon.getVisibility() == VISIBLE) {
|
|
tryUpdateBadgeBounds(icon);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected int getSuggestedMinimumWidth() {
|
|
LayoutParams labelGroupParams = (LayoutParams) labelGroup.getLayoutParams();
|
|
int labelWidth =
|
|
labelGroupParams.leftMargin + labelGroup.getMeasuredWidth() + labelGroupParams.rightMargin;
|
|
|
|
return max(getSuggestedIconWidth(), labelWidth);
|
|
}
|
|
|
|
@Override
|
|
protected int getSuggestedMinimumHeight() {
|
|
LayoutParams labelGroupParams = (LayoutParams) labelGroup.getLayoutParams();
|
|
return getSuggestedIconHeight()
|
|
+ labelGroupParams.topMargin
|
|
+ labelGroup.getMeasuredHeight()
|
|
+ labelGroupParams.bottomMargin;
|
|
}
|
|
|
|
@Override
|
|
public void initialize(@NonNull MenuItemImpl itemData, int menuType) {
|
|
this.itemData = itemData;
|
|
setCheckable(itemData.isCheckable());
|
|
setChecked(itemData.isChecked());
|
|
setEnabled(itemData.isEnabled());
|
|
setIcon(itemData.getIcon());
|
|
setTitle(itemData.getTitle());
|
|
setId(itemData.getItemId());
|
|
if (!TextUtils.isEmpty(itemData.getContentDescription())) {
|
|
setContentDescription(itemData.getContentDescription());
|
|
}
|
|
|
|
CharSequence tooltipText =
|
|
!TextUtils.isEmpty(itemData.getTooltipText())
|
|
? itemData.getTooltipText()
|
|
: itemData.getTitle();
|
|
|
|
// Avoid calling tooltip for L and M devices because long pressing twice may freeze devices.
|
|
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP || VERSION.SDK_INT > VERSION_CODES.M) {
|
|
TooltipCompat.setTooltipText(this, tooltipText);
|
|
}
|
|
setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
|
|
this.initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Remove state so this View can be reused.
|
|
*
|
|
* <p>Item Views are held in a pool and reused when the number of menu items to be shown changes.
|
|
* This will be called when this View is released from the pool.
|
|
*
|
|
* @see NavigationBarMenuView#buildMenuView()
|
|
*/
|
|
void clear() {
|
|
this.removeBadge();
|
|
this.itemData = null;
|
|
this.activeIndicatorProgress = 0;
|
|
this.initialized = false;
|
|
}
|
|
|
|
/**
|
|
* If this item's layout contains a container which holds the icon and active indicator, return
|
|
* the container. Otherwise, return the icon image view.
|
|
*
|
|
* <p>This is needed for clients who subclass this view and set their own item layout resource
|
|
* which might not container an icon container or active indicator view.
|
|
*/
|
|
private View getIconOrContainer() {
|
|
return iconContainer != null ? iconContainer : icon;
|
|
}
|
|
|
|
public void setItemPosition(int position) {
|
|
itemPosition = position;
|
|
}
|
|
|
|
public int getItemPosition() {
|
|
return itemPosition;
|
|
}
|
|
|
|
public void setShifting(boolean shifting) {
|
|
if (isShifting != shifting) {
|
|
isShifting = shifting;
|
|
refreshChecked();
|
|
}
|
|
}
|
|
|
|
public void setLabelVisibilityMode(@NavigationBarView.LabelVisibility int mode) {
|
|
if (labelVisibilityMode != mode) {
|
|
labelVisibilityMode = mode;
|
|
updateActiveIndicatorTransform();
|
|
updateActiveIndicatorLayoutParams(getWidth());
|
|
refreshChecked();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public MenuItemImpl getItemData() {
|
|
return itemData;
|
|
}
|
|
|
|
@Override
|
|
public void setTitle(@Nullable CharSequence title) {
|
|
smallLabel.setText(title);
|
|
largeLabel.setText(title);
|
|
if (itemData == null || TextUtils.isEmpty(itemData.getContentDescription())) {
|
|
setContentDescription(title);
|
|
}
|
|
|
|
CharSequence tooltipText =
|
|
itemData == null || TextUtils.isEmpty(itemData.getTooltipText())
|
|
? title
|
|
: itemData.getTooltipText();
|
|
// Avoid calling tooltip for L and M devices because long pressing twice may freeze devices.
|
|
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP || VERSION.SDK_INT > VERSION_CODES.M) {
|
|
TooltipCompat.setTooltipText(this, tooltipText);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setCheckable(boolean checkable) {
|
|
refreshDrawableState();
|
|
}
|
|
|
|
@Override
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
super.onSizeChanged(w, h, oldw, oldh);
|
|
// Update the width of the active indicator to fit within the bounds of its parent. This is
|
|
// needed when there is not enough width to accommodate the desired width of the indicator. Post
|
|
// this update in order to wait for parent layout changes to actually take effect before
|
|
// setting the new width.
|
|
final int width = w;
|
|
post(
|
|
new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
updateActiveIndicatorLayoutParams(width);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void updateActiveIndicatorTransform() {
|
|
if (isActiveIndicatorResizeableAndUnlabeled()) {
|
|
activeIndicatorTransform = ACTIVE_INDICATOR_UNLABELED_TRANSFORM;
|
|
} else {
|
|
activeIndicatorTransform = ACTIVE_INDICATOR_LABELED_TRANSFORM;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the active indicator for a given 0-1 value.
|
|
*
|
|
* @param progress 0 when the indicator should communicate an unselected state (typically gone), 1
|
|
* when the indicator should communicate a selected state (typically showing at its full width
|
|
* and height).
|
|
* @param target The final value towards which progress is animating. This can be used to
|
|
* determine if the indicator is being unselected or selected.
|
|
*/
|
|
private void setActiveIndicatorProgress(
|
|
@FloatRange(from = 0F, to = 1F) float progress, float target) {
|
|
if (activeIndicatorView != null) {
|
|
activeIndicatorTransform.updateForProgress(progress, target, activeIndicatorView);
|
|
}
|
|
activeIndicatorProgress = progress;
|
|
}
|
|
|
|
/** If the active indicator is enabled, animate from it's current state to it's new state. */
|
|
private void maybeAnimateActiveIndicatorToProgress(
|
|
@FloatRange(from = 0F, to = 1F) final float newProgress) {
|
|
// If the active indicator is disabled or this view is in the process of being initialized,
|
|
// jump the active indicator to it's final state.
|
|
if (!activeIndicatorEnabled || !initialized || !ViewCompat.isAttachedToWindow(this)) {
|
|
setActiveIndicatorProgress(newProgress, newProgress);
|
|
return;
|
|
}
|
|
|
|
if (activeIndicatorAnimator != null) {
|
|
activeIndicatorAnimator.cancel();
|
|
activeIndicatorAnimator = null;
|
|
}
|
|
activeIndicatorAnimator = ValueAnimator.ofFloat(activeIndicatorProgress, newProgress);
|
|
activeIndicatorAnimator.addUpdateListener(
|
|
new AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
float progress = (float) animation.getAnimatedValue();
|
|
setActiveIndicatorProgress(progress, newProgress);
|
|
}
|
|
});
|
|
activeIndicatorAnimator.setInterpolator(
|
|
MotionUtils.resolveThemeInterpolator(
|
|
getContext(),
|
|
R.attr.motionEasingStandard,
|
|
AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR));
|
|
activeIndicatorAnimator.setDuration(
|
|
MotionUtils.resolveThemeDuration(
|
|
getContext(),
|
|
R.attr.motionDurationLong1,
|
|
getResources().getInteger(R.integer.material_motion_duration_long_1)));
|
|
activeIndicatorAnimator.start();
|
|
}
|
|
|
|
/**
|
|
* Refresh the state of this item if it has been initialized.
|
|
*
|
|
* <p>This is useful if parameters calculated based on this item's checked state (label
|
|
* visibility, indicator state, iconContainer position) have changed and should be recalculated.
|
|
*/
|
|
private void refreshChecked() {
|
|
if (itemData != null) {
|
|
setChecked(itemData.isChecked());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setChecked(boolean checked) {
|
|
largeLabel.setPivotX(largeLabel.getWidth() / 2);
|
|
largeLabel.setPivotY(largeLabel.getBaseline());
|
|
smallLabel.setPivotX(smallLabel.getWidth() / 2);
|
|
smallLabel.setPivotY(smallLabel.getBaseline());
|
|
|
|
float newIndicatorProgress = checked ? 1F : 0F;
|
|
maybeAnimateActiveIndicatorToProgress(newIndicatorProgress);
|
|
|
|
switch (labelVisibilityMode) {
|
|
case NavigationBarView.LABEL_VISIBILITY_AUTO:
|
|
if (isShifting) {
|
|
if (checked) {
|
|
// Show icon and large label
|
|
setViewTopMarginAndGravity(
|
|
getIconOrContainer(), itemPaddingTop, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
|
|
updateViewPaddingBottom(labelGroup, itemPaddingBottom);
|
|
largeLabel.setVisibility(VISIBLE);
|
|
} else {
|
|
// Show icon
|
|
setViewTopMarginAndGravity(getIconOrContainer(), itemPaddingTop, Gravity.CENTER);
|
|
updateViewPaddingBottom(labelGroup, 0);
|
|
largeLabel.setVisibility(INVISIBLE);
|
|
}
|
|
smallLabel.setVisibility(INVISIBLE);
|
|
} else {
|
|
updateViewPaddingBottom(labelGroup, itemPaddingBottom);
|
|
if (checked) {
|
|
// Show icon and large label
|
|
setViewTopMarginAndGravity(
|
|
getIconOrContainer(),
|
|
(int) (itemPaddingTop + shiftAmount),
|
|
Gravity.CENTER_HORIZONTAL | Gravity.TOP);
|
|
setViewScaleValues(largeLabel, 1f, 1f, VISIBLE);
|
|
setViewScaleValues(smallLabel, scaleUpFactor, scaleUpFactor, INVISIBLE);
|
|
} else {
|
|
// Show icon and small label
|
|
setViewTopMarginAndGravity(
|
|
getIconOrContainer(), itemPaddingTop, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
|
|
setViewScaleValues(largeLabel, scaleDownFactor, scaleDownFactor, INVISIBLE);
|
|
setViewScaleValues(smallLabel, 1f, 1f, VISIBLE);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NavigationBarView.LABEL_VISIBILITY_SELECTED:
|
|
if (checked) {
|
|
// Show icon and large label
|
|
setViewTopMarginAndGravity(
|
|
getIconOrContainer(), itemPaddingTop, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
|
|
updateViewPaddingBottom(labelGroup, itemPaddingBottom);
|
|
largeLabel.setVisibility(VISIBLE);
|
|
} else {
|
|
// Show icon only
|
|
setViewTopMarginAndGravity(getIconOrContainer(), itemPaddingTop, Gravity.CENTER);
|
|
updateViewPaddingBottom(labelGroup, 0);
|
|
largeLabel.setVisibility(INVISIBLE);
|
|
}
|
|
smallLabel.setVisibility(INVISIBLE);
|
|
break;
|
|
|
|
case NavigationBarView.LABEL_VISIBILITY_LABELED:
|
|
updateViewPaddingBottom(labelGroup, itemPaddingBottom);
|
|
if (checked) {
|
|
// Show icon and large label
|
|
setViewTopMarginAndGravity(
|
|
getIconOrContainer(),
|
|
(int) (itemPaddingTop + shiftAmount),
|
|
Gravity.CENTER_HORIZONTAL | Gravity.TOP);
|
|
setViewScaleValues(largeLabel, 1f, 1f, VISIBLE);
|
|
setViewScaleValues(smallLabel, scaleUpFactor, scaleUpFactor, INVISIBLE);
|
|
} else {
|
|
// Show icon and small label
|
|
setViewTopMarginAndGravity(
|
|
getIconOrContainer(), itemPaddingTop, Gravity.CENTER_HORIZONTAL | Gravity.TOP);
|
|
setViewScaleValues(largeLabel, scaleDownFactor, scaleDownFactor, INVISIBLE);
|
|
setViewScaleValues(smallLabel, 1f, 1f, VISIBLE);
|
|
}
|
|
break;
|
|
|
|
case NavigationBarView.LABEL_VISIBILITY_UNLABELED:
|
|
// Show icon only
|
|
setViewTopMarginAndGravity(getIconOrContainer(), itemPaddingTop, Gravity.CENTER);
|
|
largeLabel.setVisibility(GONE);
|
|
smallLabel.setVisibility(GONE);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
refreshDrawableState();
|
|
|
|
// Set the item as selected to send an AccessibilityEvent.TYPE_VIEW_SELECTED from View, so that
|
|
// the item is read out as selected.
|
|
setSelected(checked);
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
|
|
super.onInitializeAccessibilityNodeInfo(info);
|
|
if (badgeDrawable != null && badgeDrawable.isVisible()) {
|
|
CharSequence customContentDescription = itemData.getTitle();
|
|
if (!TextUtils.isEmpty(itemData.getContentDescription())) {
|
|
customContentDescription = itemData.getContentDescription();
|
|
}
|
|
info.setContentDescription(
|
|
customContentDescription + ", " + badgeDrawable.getContentDescription());
|
|
}
|
|
AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info);
|
|
infoCompat.setCollectionItemInfo(
|
|
CollectionItemInfoCompat.obtain(
|
|
/* rowIndex= */ 0,
|
|
/* rowSpan= */ 1,
|
|
/* columnIndex= */ getItemVisiblePosition(),
|
|
/* columnSpan= */ 1,
|
|
/* heading= */ false,
|
|
/* selected= */ isSelected()));
|
|
if (isSelected()) {
|
|
infoCompat.setClickable(false);
|
|
infoCompat.removeAction(AccessibilityActionCompat.ACTION_CLICK);
|
|
}
|
|
infoCompat.setRoleDescription(getResources().getString(R.string.item_view_role_description));
|
|
}
|
|
|
|
/**
|
|
* Iterate through all the preceding bottom navigating items to determine this item's visible
|
|
* position.
|
|
*
|
|
* @return This item's visible position in a bottom navigation.
|
|
*/
|
|
private int getItemVisiblePosition() {
|
|
ViewGroup parent = (ViewGroup) getParent();
|
|
int index = parent.indexOfChild(this);
|
|
int visiblePosition = 0;
|
|
for (int i = 0; i < index; i++) {
|
|
View child = parent.getChildAt(i);
|
|
if (child instanceof NavigationBarItemView && child.getVisibility() == View.VISIBLE) {
|
|
visiblePosition++;
|
|
}
|
|
}
|
|
return visiblePosition;
|
|
}
|
|
|
|
private static void setViewTopMarginAndGravity(@NonNull View view, int topMargin, int gravity) {
|
|
LayoutParams viewParams = (LayoutParams) view.getLayoutParams();
|
|
viewParams.topMargin = topMargin;
|
|
// Set the bottom margin to be equal to the top margin so this view can be centered in it's
|
|
// parent if gravity is set to CENTER.
|
|
viewParams.bottomMargin = topMargin;
|
|
viewParams.gravity = gravity;
|
|
view.setLayoutParams(viewParams);
|
|
}
|
|
|
|
private static void setViewScaleValues(
|
|
@NonNull View view, float scaleX, float scaleY, int visibility) {
|
|
view.setScaleX(scaleX);
|
|
view.setScaleY(scaleY);
|
|
view.setVisibility(visibility);
|
|
}
|
|
|
|
private static void updateViewPaddingBottom(@NonNull View view, int paddingBottom) {
|
|
view.setPadding(
|
|
view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), paddingBottom);
|
|
}
|
|
|
|
@Override
|
|
public void setEnabled(boolean enabled) {
|
|
super.setEnabled(enabled);
|
|
smallLabel.setEnabled(enabled);
|
|
largeLabel.setEnabled(enabled);
|
|
icon.setEnabled(enabled);
|
|
|
|
if (enabled) {
|
|
ViewCompat.setPointerIcon(
|
|
this, PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
|
|
} else {
|
|
ViewCompat.setPointerIcon(this, null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@NonNull
|
|
public int[] onCreateDrawableState(final int extraSpace) {
|
|
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
|
if (itemData != null && itemData.isCheckable() && itemData.isChecked()) {
|
|
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
|
|
}
|
|
return drawableState;
|
|
}
|
|
|
|
@Override
|
|
public void setShortcut(boolean showShortcut, char shortcutKey) {}
|
|
|
|
@Override
|
|
public void setIcon(@Nullable Drawable iconDrawable) {
|
|
if (iconDrawable == originalIconDrawable) {
|
|
return;
|
|
}
|
|
|
|
// Save the original icon to check if it has changed in future calls of this method.
|
|
originalIconDrawable = iconDrawable;
|
|
if (iconDrawable != null) {
|
|
Drawable.ConstantState state = iconDrawable.getConstantState();
|
|
iconDrawable =
|
|
DrawableCompat.wrap(state == null ? iconDrawable : state.newDrawable()).mutate();
|
|
wrappedIconDrawable = iconDrawable;
|
|
if (iconTint != null) {
|
|
DrawableCompat.setTintList(wrappedIconDrawable, iconTint);
|
|
}
|
|
}
|
|
this.icon.setImageDrawable(iconDrawable);
|
|
}
|
|
|
|
@Override
|
|
public boolean prefersCondensedTitle() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean showsIcon() {
|
|
return true;
|
|
}
|
|
|
|
public void setIconTintList(@Nullable ColorStateList tint) {
|
|
iconTint = tint;
|
|
if (itemData != null && wrappedIconDrawable != null) {
|
|
DrawableCompat.setTintList(wrappedIconDrawable, iconTint);
|
|
wrappedIconDrawable.invalidateSelf();
|
|
}
|
|
}
|
|
|
|
public void setIconSize(int iconSize) {
|
|
LayoutParams iconParams = (LayoutParams) icon.getLayoutParams();
|
|
iconParams.width = iconSize;
|
|
iconParams.height = iconSize;
|
|
icon.setLayoutParams(iconParams);
|
|
}
|
|
|
|
public void setTextAppearanceInactive(@StyleRes int inactiveTextAppearance) {
|
|
setTextAppearanceWithoutFontScaling(smallLabel, inactiveTextAppearance);
|
|
calculateTextScaleFactors(smallLabel.getTextSize(), largeLabel.getTextSize());
|
|
}
|
|
|
|
public void setTextAppearanceActive(@StyleRes int activeTextAppearance) {
|
|
setTextAppearanceWithoutFontScaling(largeLabel, activeTextAppearance);
|
|
calculateTextScaleFactors(smallLabel.getTextSize(), largeLabel.getTextSize());
|
|
}
|
|
|
|
/**
|
|
* Remove font scaling if the text size is in scaled pixels.
|
|
*
|
|
* <p>Labels are instead made accessible by showing a scaled tooltip on long press of a
|
|
* destination. If the given {@code textAppearance} is 0 or does not have a textSize, this method
|
|
* will not remove the existing scaling from the {@code textView}.
|
|
*/
|
|
private static void setTextAppearanceWithoutFontScaling(
|
|
TextView textView, @StyleRes int textAppearance) {
|
|
TextViewCompat.setTextAppearance(textView, textAppearance);
|
|
int unscaledSize =
|
|
MaterialResources.getUnscaledTextSize(textView.getContext(), textAppearance, 0);
|
|
if (unscaledSize != 0) {
|
|
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, unscaledSize);
|
|
}
|
|
}
|
|
|
|
public void setTextColor(@Nullable ColorStateList color) {
|
|
if (color != null) {
|
|
smallLabel.setTextColor(color);
|
|
largeLabel.setTextColor(color);
|
|
}
|
|
}
|
|
|
|
private void calculateTextScaleFactors(float smallLabelSize, float largeLabelSize) {
|
|
shiftAmount = smallLabelSize - largeLabelSize;
|
|
scaleUpFactor = 1f * largeLabelSize / smallLabelSize;
|
|
scaleDownFactor = 1f * smallLabelSize / largeLabelSize;
|
|
}
|
|
|
|
public void setItemBackground(int background) {
|
|
Drawable backgroundDrawable =
|
|
background == 0 ? null : ContextCompat.getDrawable(getContext(), background);
|
|
setItemBackground(backgroundDrawable);
|
|
}
|
|
|
|
public void setItemBackground(@Nullable Drawable background) {
|
|
if (background != null && background.getConstantState() != null) {
|
|
background = background.getConstantState().newDrawable().mutate();
|
|
}
|
|
ViewCompat.setBackground(this, background);
|
|
}
|
|
|
|
/**
|
|
* Set the padding applied to the icon/active indicator container from the top of the item view.
|
|
*/
|
|
public void setItemPaddingTop(int paddingTop) {
|
|
if (this.itemPaddingTop != paddingTop) {
|
|
this.itemPaddingTop = paddingTop;
|
|
refreshChecked();
|
|
}
|
|
}
|
|
|
|
/** Set the padding applied to the labels from the bottom of the item view. */
|
|
public void setItemPaddingBottom(int paddingBottom) {
|
|
if (this.itemPaddingBottom != paddingBottom) {
|
|
this.itemPaddingBottom = paddingBottom;
|
|
refreshChecked();
|
|
}
|
|
}
|
|
|
|
/** Set whether or not this item should show an active indicator when checked. */
|
|
public void setActiveIndicatorEnabled(boolean enabled) {
|
|
this.activeIndicatorEnabled = enabled;
|
|
if (activeIndicatorView != null) {
|
|
activeIndicatorView.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the desired width of the active indicator.
|
|
*
|
|
* <p>If the item view is not wide enough to accommodate the given {@code width} plus any
|
|
* horizontal margin, the width will be set to the width of the item view minus any horizontal
|
|
* margin.
|
|
*
|
|
* @param width The desired width of the active indicator.
|
|
*/
|
|
public void setActiveIndicatorWidth(int width) {
|
|
this.activeIndicatorDesiredWidth = width;
|
|
updateActiveIndicatorLayoutParams(getWidth());
|
|
}
|
|
|
|
/**
|
|
* Update the active indicators width and height for the available width and label visibility
|
|
* mode.
|
|
*
|
|
* @param availableWidth The total width of this item layout.
|
|
*/
|
|
private void updateActiveIndicatorLayoutParams(int availableWidth) {
|
|
// Set width to the min of either the desired indicator width or the available width minus
|
|
// a horizontal margin.
|
|
if (activeIndicatorView == null) {
|
|
return;
|
|
}
|
|
|
|
int newWidth =
|
|
min(activeIndicatorDesiredWidth, availableWidth - (activeIndicatorMarginHorizontal * 2));
|
|
|
|
LayoutParams indicatorParams = (LayoutParams) activeIndicatorView.getLayoutParams();
|
|
// If the label visibility is unlabeled, make the active indicator's height equal to it's width.
|
|
indicatorParams.height =
|
|
isActiveIndicatorResizeableAndUnlabeled() ? newWidth : activeIndicatorDesiredHeight;
|
|
indicatorParams.width = newWidth;
|
|
activeIndicatorView.setLayoutParams(indicatorParams);
|
|
}
|
|
|
|
private boolean isActiveIndicatorResizeableAndUnlabeled() {
|
|
return activeIndicatorResizeable
|
|
&& labelVisibilityMode == NavigationBarView.LABEL_VISIBILITY_UNLABELED;
|
|
}
|
|
|
|
/**
|
|
* Set the desired height of the active indicator.
|
|
*
|
|
* <p>TODO: Consider adjusting based on available height
|
|
*
|
|
* @param height The desired height of the active indicator.
|
|
*/
|
|
public void setActiveIndicatorHeight(int height) {
|
|
activeIndicatorDesiredHeight = height;
|
|
updateActiveIndicatorLayoutParams(getWidth());
|
|
}
|
|
|
|
/**
|
|
* 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 this item view.
|
|
*
|
|
* @see #updateActiveIndicatorLayoutParams(int)
|
|
* @param marginHorizontal The horizontal margin, in pixels.
|
|
*/
|
|
public void setActiveIndicatorMarginHorizontal(@Px int marginHorizontal) {
|
|
this.activeIndicatorMarginHorizontal = marginHorizontal;
|
|
updateActiveIndicatorLayoutParams(getWidth());
|
|
}
|
|
|
|
/** Get the drawable used as the active indicator. */
|
|
@Nullable
|
|
public Drawable getActiveIndicatorDrawable() {
|
|
if (activeIndicatorView == null) {
|
|
return null;
|
|
}
|
|
|
|
return activeIndicatorView.getBackground();
|
|
}
|
|
|
|
/** Set the drawable to be used as the active indicator. */
|
|
public void setActiveIndicatorDrawable(@Nullable Drawable activeIndicatorDrawable) {
|
|
if (activeIndicatorView == null) {
|
|
return;
|
|
}
|
|
|
|
activeIndicatorView.setBackgroundDrawable(activeIndicatorDrawable);
|
|
}
|
|
|
|
/** Set whether the indicator can be automatically resized. */
|
|
public void setActiveIndicatorResizeable(boolean resizeable) {
|
|
this.activeIndicatorResizeable = resizeable;
|
|
}
|
|
|
|
void setBadge(@NonNull BadgeDrawable badgeDrawable) {
|
|
if (this.badgeDrawable == badgeDrawable) {
|
|
return;
|
|
}
|
|
if (hasBadge() && icon != null) {
|
|
Log.w("NavigationBar", "Multiple badges shouldn't be attached to one item.");
|
|
tryRemoveBadgeFromAnchor(icon);
|
|
}
|
|
this.badgeDrawable = badgeDrawable;
|
|
if (icon != null) {
|
|
tryAttachBadgeToAnchor(icon);
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
public BadgeDrawable getBadge() {
|
|
return this.badgeDrawable;
|
|
}
|
|
|
|
void removeBadge() {
|
|
tryRemoveBadgeFromAnchor(icon);
|
|
}
|
|
|
|
private boolean hasBadge() {
|
|
return badgeDrawable != null;
|
|
}
|
|
|
|
private void tryUpdateBadgeBounds(View anchorView) {
|
|
if (!hasBadge()) {
|
|
return;
|
|
}
|
|
BadgeUtils.setBadgeDrawableBounds(
|
|
badgeDrawable, anchorView, getCustomParentForBadge(anchorView));
|
|
}
|
|
|
|
private void tryAttachBadgeToAnchor(@Nullable View anchorView) {
|
|
if (!hasBadge()) {
|
|
return;
|
|
}
|
|
if (anchorView != null) {
|
|
// Avoid clipping a badge if it's displayed.
|
|
setClipChildren(false);
|
|
setClipToPadding(false);
|
|
|
|
BadgeUtils.attachBadgeDrawable(
|
|
badgeDrawable, anchorView, getCustomParentForBadge(anchorView));
|
|
}
|
|
}
|
|
|
|
private void tryRemoveBadgeFromAnchor(@Nullable View anchorView) {
|
|
if (!hasBadge()) {
|
|
return;
|
|
}
|
|
if (anchorView != null) {
|
|
// Clip children / view to padding when no badge is displayed.
|
|
setClipChildren(true);
|
|
setClipToPadding(true);
|
|
|
|
BadgeUtils.detachBadgeDrawable(badgeDrawable, anchorView);
|
|
}
|
|
badgeDrawable = null;
|
|
}
|
|
|
|
@Nullable
|
|
private FrameLayout getCustomParentForBadge(View anchorView) {
|
|
if (anchorView == icon) {
|
|
return BadgeUtils.USE_COMPAT_PARENT ? ((FrameLayout) icon.getParent()) : null;
|
|
}
|
|
// TODO(b/138148581): Support displaying a badge on label-only bottom navigation views.
|
|
return null;
|
|
}
|
|
|
|
private int getSuggestedIconWidth() {
|
|
int badgeWidth =
|
|
badgeDrawable == null
|
|
? 0
|
|
: badgeDrawable.getMinimumWidth() - badgeDrawable.getHorizontalOffset();
|
|
|
|
// Account for the fact that the badge may fit within the left or right margin. Give the same
|
|
// space of either side so that icon position does not move if badge gravity is changed.
|
|
LayoutParams iconContainerParams = (LayoutParams) getIconOrContainer().getLayoutParams();
|
|
return max(badgeWidth, iconContainerParams.leftMargin)
|
|
+ icon.getMeasuredWidth()
|
|
+ max(badgeWidth, iconContainerParams.rightMargin);
|
|
}
|
|
|
|
private int getSuggestedIconHeight() {
|
|
int badgeHeight = 0;
|
|
if (badgeDrawable != null) {
|
|
badgeHeight = badgeDrawable.getMinimumHeight() / 2;
|
|
}
|
|
|
|
// Account for the fact that the badge may fit within the top margin. Bottom margin is ignored
|
|
// because the icon view will be aligned to the baseline of the label group. But give space for
|
|
// the badge at the bottom as well, so that icon does not move if badge gravity is changed.
|
|
LayoutParams iconContainerParams = (LayoutParams) getIconOrContainer().getLayoutParams();
|
|
return max(badgeHeight, iconContainerParams.topMargin) + icon.getMeasuredWidth() + badgeHeight;
|
|
}
|
|
|
|
/**
|
|
* Returns the unique identifier to the drawable resource that must be used to render background
|
|
* of the menu item view. Override this if the subclassed menu item requires a different
|
|
* background resource to be set.
|
|
*/
|
|
@DrawableRes
|
|
protected int getItemBackgroundResId() {
|
|
return R.drawable.mtrl_navigation_bar_item_background;
|
|
}
|
|
|
|
/**
|
|
* Returns the unique identifier to the dimension resource that will specify the default margin
|
|
* this menu item view. Override this if the subclassed menu item requires a different default
|
|
* margin value.
|
|
*/
|
|
@DimenRes
|
|
protected int getItemDefaultMarginResId() {
|
|
return R.dimen.mtrl_navigation_bar_item_default_margin;
|
|
}
|
|
|
|
/**
|
|
* Returns the unique identifier to the layout resource that must be used to render the items in
|
|
* this menu item view.
|
|
*/
|
|
@LayoutRes
|
|
protected abstract int getItemLayoutResId();
|
|
|
|
/**
|
|
* A class used to manipulate the {@link NavigationBarItemView}'s active indicator view when
|
|
* animating between hidden and shown.
|
|
*
|
|
* <p>By default, this class scales the indicator in the x direction to reveal the default pill
|
|
* shape.
|
|
*
|
|
* <p>Subclasses can override {@link #updateForProgress(float, float, View)} to manipulate the
|
|
* view in any way appropriate.
|
|
*/
|
|
private static class ActiveIndicatorTransform {
|
|
|
|
private static final float SCALE_X_HIDDEN = .4F;
|
|
private static final float SCALE_X_SHOWN = 1F;
|
|
|
|
// The fraction of the animation's total duration over which the indicator will be faded in or
|
|
// out.
|
|
private static final float ALPHA_FRACTION = 1F / 5F;
|
|
|
|
/**
|
|
* Calculate the alpha value, based on a progress and target value, that has the indicator
|
|
* appear or disappear over the first 1/5th of the transform.
|
|
*/
|
|
protected float calculateAlpha(
|
|
@FloatRange(from = 0F, to = 1F) float progress,
|
|
@FloatRange(from = 0F, to = 1F) float targetValue) {
|
|
// Animate the alpha of the indicator over the first ALPHA_FRACTION of the animation
|
|
float startAlphaFraction = targetValue == 0F ? 1F - ALPHA_FRACTION : 0F;
|
|
float endAlphaFraction = targetValue == 0F ? 1F : 0F + ALPHA_FRACTION;
|
|
return AnimationUtils.lerp(0F, 1F, startAlphaFraction, endAlphaFraction, progress);
|
|
}
|
|
|
|
protected float calculateScaleX(
|
|
@FloatRange(from = 0F, to = 1F) float progress,
|
|
@FloatRange(from = 0F, to = 1F) float targetValue) {
|
|
return AnimationUtils.lerp(SCALE_X_HIDDEN, SCALE_X_SHOWN, progress);
|
|
}
|
|
|
|
protected float calculateScaleY(
|
|
@FloatRange(from = 0F, to = 1F) float progress,
|
|
@FloatRange(from = 0F, to = 1F) float targetValue) {
|
|
return 1F;
|
|
}
|
|
|
|
/**
|
|
* Called whenever the {@code indicator} should update its parameters (scale, alpha, etc.) in
|
|
* response to a change in progress.
|
|
*
|
|
* @param progress A value between 0 and 1 where 0 represents a fully hidden indicator and 1
|
|
* indicates a fully shown indicator.
|
|
* @param targetValue The final value towards which the progress is moving. This will be either
|
|
* 0 and 1 and can be used to determine whether the indicator is showing or hiding if show
|
|
* and hide animations differ.
|
|
* @param indicator The active indicator {@link View}.
|
|
*/
|
|
public void updateForProgress(
|
|
@FloatRange(from = 0F, to = 1F) float progress,
|
|
@FloatRange(from = 0F, to = 1F) float targetValue,
|
|
@NonNull View indicator) {
|
|
indicator.setScaleX(calculateScaleX(progress, targetValue));
|
|
indicator.setScaleY(calculateScaleY(progress, targetValue));
|
|
indicator.setAlpha(calculateAlpha(progress, targetValue));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A transform class used to animate the active indicator of a {@link NavigationBarItemView} that
|
|
* is unlabeled.
|
|
*
|
|
* <p>This differs from the default {@link ActiveIndicatorTransform} class by uniformly scaling in
|
|
* the X and Y axis.
|
|
*/
|
|
private static class ActiveIndicatorUnlabeledTransform extends ActiveIndicatorTransform {
|
|
|
|
@Override
|
|
protected float calculateScaleY(float progress, float targetValue) {
|
|
return calculateScaleX(progress, targetValue);
|
|
}
|
|
}
|
|
}
|