diff --git a/lib/java/com/google/android/material/navigation/NavigationBarItemView.java b/lib/java/com/google/android/material/navigation/NavigationBarItemView.java index 7e6cdebf181..77bd195cc5e 100644 --- a/lib/java/com/google/android/material/navigation/NavigationBarItemView.java +++ b/lib/java/com/google/android/material/navigation/NavigationBarItemView.java @@ -27,6 +27,8 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import androidx.appcompat.view.menu.MenuItemImpl; @@ -37,6 +39,7 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; @@ -65,6 +68,7 @@ import com.google.android.material.badge.BadgeUtils; import com.google.android.material.motion.MotionUtils; import com.google.android.material.resources.MaterialResources; +import com.google.android.material.ripple.RippleUtils; /** * Provides a view that will be used to render destination items inside a {@link @@ -78,6 +82,8 @@ public abstract class NavigationBarItemView extends FrameLayout implements MenuV private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; private boolean initialized = false; + private ColorStateList itemRippleColor; + @Nullable Drawable itemBackground; private int itemPaddingTop; private int itemPaddingBottom; private float shiftAmount; @@ -672,7 +678,82 @@ public void setItemBackground(@Nullable Drawable background) { if (background != null && background.getConstantState() != null) { background = background.getConstantState().newDrawable().mutate(); } - ViewCompat.setBackground(this, background); + this.itemBackground = background; + refreshItemBackground(); + } + + public void setItemRippleColor(@Nullable ColorStateList itemRippleColor) { + this.itemRippleColor = itemRippleColor; + refreshItemBackground(); + } + + /** + * Update this item's ripple behavior given the current configuration. + * + *

If an active indicator is being used, a ripple is added to the active indicator. Otherwise, + * if a custom background has not been set, a default background that works across all API levels + * is created and set. + */ + private void refreshItemBackground() { + Drawable iconContainerBackgroundDrawable = null; + Drawable itemBackgroundDrawable = itemBackground; + boolean defaultHighlightEnabled = true; + + if (itemRippleColor != null) { + Drawable maskDrawable = getActiveIndicatorDrawable(); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP + && activeIndicatorEnabled + && getActiveIndicatorDrawable() != null + && iconContainer != null + && maskDrawable != null) { + + // Remove the default focus highlight that highlights the entire view and rely on the + // active indicator ripple to communicate state. + defaultHighlightEnabled = false; + // Set the icon container's background to a ripple masked by the active indicator's + // drawable. + iconContainerBackgroundDrawable = + new RippleDrawable( + RippleUtils.sanitizeRippleDrawableColor(itemRippleColor), null, maskDrawable); + } else if (itemBackgroundDrawable == null) { + // If there has not been a custom background set, use a fallback item background to display + // state over the entire item. + itemBackgroundDrawable = createItemBackgroundCompat(itemRippleColor); + } + } + // Check that this item includes an icon container. If a NavigationBarView's subclass supplies + // a custom item layout, this can be null. + if (iconContainer != null) { + ViewCompat.setBackground(iconContainer, iconContainerBackgroundDrawable); + } + ViewCompat.setBackground(this, itemBackgroundDrawable); + if (VERSION.SDK_INT >= VERSION_CODES.O) { + setDefaultFocusHighlightEnabled(defaultHighlightEnabled); + } + } + + /** + * Create a {@link Drawable} to be used as this item's background when a an active indicator is + * not in use or a custom item background has not been set. + * + * @return a {@link Drawable} that can be used as a background and display state. + */ + private static Drawable createItemBackgroundCompat(@NonNull ColorStateList rippleColor) { + ColorStateList rippleDrawableColor = RippleUtils.convertToRippleDrawableColor(rippleColor); + Drawable backgroundDrawable; + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + backgroundDrawable = new RippleDrawable(rippleDrawableColor, null, null); + } else { + GradientDrawable rippleDrawable = new GradientDrawable(); + // TODO: Find a workaround for this. Currently on certain devices/versions, LayerDrawable + // will draw a black background underneath any layer with a non-opaque color, + // (e.g. ripple) unless we set the shape to be something that's not a perfect rectangle. + rippleDrawable.setCornerRadius(0.00001F); + Drawable rippleDrawableCompat = DrawableCompat.wrap(rippleDrawable); + DrawableCompat.setTintList(rippleDrawableCompat, rippleDrawableColor); + backgroundDrawable = rippleDrawableCompat; + } + return backgroundDrawable; } /** @@ -696,6 +777,7 @@ public void setItemPaddingBottom(int paddingBottom) { /** Set whether or not this item should show an active indicator when checked. */ public void setActiveIndicatorEnabled(boolean enabled) { this.activeIndicatorEnabled = enabled; + refreshItemBackground(); if (activeIndicatorView != null) { activeIndicatorView.setVisibility(enabled ? View.VISIBLE : View.GONE); requestLayout(); @@ -786,6 +868,16 @@ public void setActiveIndicatorDrawable(@Nullable Drawable activeIndicatorDrawabl } activeIndicatorView.setBackgroundDrawable(activeIndicatorDrawable); + refreshItemBackground(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // Pass touch events through to the icon container so the active indicator ripple can be shown. + if (iconContainer != null && activeIndicatorEnabled) { + iconContainer.dispatchTouchEvent(ev); + } + return super.dispatchTouchEvent(ev); } /** Set whether the indicator can be automatically resized. */ diff --git a/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java b/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java index cf98ede08b4..09ad511dd39 100644 --- a/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java +++ b/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java @@ -90,6 +90,7 @@ public abstract class NavigationBarMenuView extends ViewGroup implements MenuVie @StyleRes private int itemTextAppearanceInactive; @StyleRes private int itemTextAppearanceActive; private Drawable itemBackground; + @Nullable private ColorStateList itemRippleColor; private int itemBackgroundRes; @NonNull private final SparseArray badgeDrawables = new SparseArray<>(ITEM_POOL_SIZE); @@ -562,6 +563,32 @@ public void setItemBackground(@Nullable Drawable 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 (NavigationBarItemView item : buttons) { + 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. * @@ -701,6 +728,7 @@ public void buildMenuView() { } else { child.setItemBackground(itemBackgroundRes); } + child.setItemRippleColor(itemRippleColor); child.setShifting(shifting); child.setLabelVisibilityMode(labelVisibilityMode); MenuItemImpl item = (MenuItemImpl) menu.getItem(i); diff --git a/lib/java/com/google/android/material/navigation/NavigationBarView.java b/lib/java/com/google/android/material/navigation/NavigationBarView.java index 80996e7713a..e516a256268 100644 --- a/lib/java/com/google/android/material/navigation/NavigationBarView.java +++ b/lib/java/com/google/android/material/navigation/NavigationBarView.java @@ -27,8 +27,6 @@ import android.content.res.TypedArray; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.RippleDrawable; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -60,7 +58,6 @@ import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.resources.MaterialResources; -import com.google.android.material.ripple.RippleUtils; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.MaterialShapeUtils; import com.google.android.material.shape.ShapeAppearanceModel; @@ -128,7 +125,6 @@ public abstract class NavigationBarView extends FrameLayout { @NonNull private final NavigationBarMenu menu; @NonNull private final NavigationBarMenuView menuView; @NonNull private final NavigationBarPresenter presenter = new NavigationBarPresenter(); - @Nullable private ColorStateList itemRippleColor; private MenuInflater menuInflater; private OnItemSelectedListener selectedListener; @@ -490,7 +486,6 @@ public int getItemBackgroundResource() { */ public void setItemBackgroundResource(@DrawableRes int resId) { menuView.setItemBackgroundRes(resId); - itemRippleColor = null; } /** @@ -515,7 +510,6 @@ public Drawable getItemBackground() { */ public void setItemBackground(@Nullable Drawable background) { menuView.setItemBackground(background); - itemRippleColor = null; } /** @@ -527,7 +521,7 @@ public void setItemBackground(@Nullable Drawable background) { */ @Nullable public ColorStateList getItemRippleColor() { - return itemRippleColor; + return menuView.getItemRippleColor(); } /** @@ -539,33 +533,7 @@ public ColorStateList getItemRippleColor() { * @attr ref R.styleable#BottomNavigationView_itemRippleColor */ public void setItemRippleColor(@Nullable ColorStateList itemRippleColor) { - if (this.itemRippleColor == itemRippleColor) { - // Clear the item background when setItemRippleColor(null) is called for consistency. - if (itemRippleColor == null && menuView.getItemBackground() != null) { - menuView.setItemBackground(null); - } - return; - } - - this.itemRippleColor = itemRippleColor; - if (itemRippleColor == null) { - menuView.setItemBackground(null); - } else { - ColorStateList rippleDrawableColor = - RippleUtils.convertToRippleDrawableColor(itemRippleColor); - if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - menuView.setItemBackground(new RippleDrawable(rippleDrawableColor, null, null)); - } else { - GradientDrawable rippleDrawable = new GradientDrawable(); - // TODO: Find a workaround for this. Currently on certain devices/versions, LayerDrawable - // will draw a black background underneath any layer with a non-opaque color, - // (e.g. ripple) unless we set the shape to be something that's not a perfect rectangle. - rippleDrawable.setCornerRadius(0.00001F); - Drawable rippleDrawableCompat = DrawableCompat.wrap(rippleDrawable); - DrawableCompat.setTintList(rippleDrawableCompat, rippleDrawableColor); - menuView.setItemBackground(rippleDrawableCompat); - } - } + menuView.setItemRippleColor(itemRippleColor); } /**