diff --git a/lib/java/com/google/android/material/color/MaterialColors.java b/lib/java/com/google/android/material/color/MaterialColors.java index e7b27fe6093..45e339d04de 100644 --- a/lib/java/com/google/android/material/color/MaterialColors.java +++ b/lib/java/com/google/android/material/color/MaterialColors.java @@ -20,6 +20,7 @@ import static android.graphics.Color.TRANSPARENT; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.Color; import android.util.TypedValue; import android.view.View; @@ -111,6 +112,23 @@ public static int getColor( } } + /** + * Returns the color state list for the provided theme color attribute, or the default value if + * the attribute is not set in the current theme. + */ + @NonNull + public static ColorStateList getColorStateList( + @NonNull Context context, + @AttrRes int colorAttributeResId, + @NonNull ColorStateList defaultValue) { + ColorStateList resolvedColor = null; + TypedValue typedValue = MaterialAttributes.resolve(context, colorAttributeResId); + if (typedValue != null) { + resolvedColor = resolveColorStateList(context, typedValue); + } + return resolvedColor == null ? defaultValue : resolvedColor; + } + private static int resolveColor(@NonNull Context context, @NonNull TypedValue typedValue) { if (typedValue.resourceId != 0) { // Color State List @@ -121,6 +139,15 @@ private static int resolveColor(@NonNull Context context, @NonNull TypedValue ty } } + private static ColorStateList resolveColorStateList( + @NonNull Context context, @NonNull TypedValue typedValue) { + if (typedValue.resourceId != 0) { + return ContextCompat.getColorStateList(context, typedValue.resourceId); + } else { + return ColorStateList.valueOf(typedValue.data); + } + } + /** * Convenience method that calculates {@link MaterialColors#layer(View, int, int, float)} without * an {@code overlayAlpha} value by passing in {@code 1f} for the alpha value. diff --git a/lib/java/com/google/android/material/ripple/RippleUtils.java b/lib/java/com/google/android/material/ripple/RippleUtils.java index 66871e7e3df..6cd1669dfc8 100644 --- a/lib/java/com/google/android/material/ripple/RippleUtils.java +++ b/lib/java/com/google/android/material/ripple/RippleUtils.java @@ -16,20 +16,32 @@ package com.google.android.material.ripple; +import com.google.android.material.R; + import android.annotation.TargetApi; +import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.RippleDrawable; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.util.Log; import android.util.StateSet; +import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.ColorInt; +import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.annotation.VisibleForTesting; import androidx.core.graphics.ColorUtils; +import com.google.android.material.color.MaterialColors; /** * Utils class for ripples. @@ -39,6 +51,7 @@ @RestrictTo(Scope.LIBRARY_GROUP) public class RippleUtils { + @ChecksSdkIntAtLeast(api = VERSION_CODES.LOLLIPOP) public static final boolean USE_FRAMEWORK_RIPPLE = VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP; private static final int[] PRESSED_STATE_SET = { @@ -239,6 +252,18 @@ public static boolean shouldDrawRippleCompat(@NonNull int[] stateSet) { return enabled && interactedState; } + /** + * On API 21 and 22, the ripple implementation has a bug that it will be shown behind the + * container view under certain conditions. Adding a mask when creating {@link RippleDrawable} + * solves this. Besides that since {@link RippleDrawable} doesn't support radius setting on + * Lollipop, adding masks will make the circle ripple size fit into the view boundary. + */ + @RequiresApi(VERSION_CODES.LOLLIPOP) + @NonNull + public static Drawable createOvalRippleLollipop(@NonNull Context context, @Px int padding) { + return RippleUtilsLollipop.createOvalRipple(context, padding); + } + @ColorInt private static int getColorForState(@Nullable ColorStateList rippleColor, int[] state) { int color; @@ -260,4 +285,23 @@ private static int doubleAlpha(@ColorInt int color) { int alpha = Math.min(2 * Color.alpha(color), 255); return ColorUtils.setAlphaComponent(color, alpha); } + + @RequiresApi(VERSION_CODES.LOLLIPOP) + private static class RippleUtilsLollipop { + + // Note: we need to return Drawable here to maintain API compatibility + @DoNotInline + private static Drawable createOvalRipple(@NonNull Context context, @Px int padding) { + GradientDrawable maskDrawable = new GradientDrawable(); + maskDrawable.setColor(Color.WHITE); + maskDrawable.setShape(GradientDrawable.OVAL); + InsetDrawable maskWithPaddings = + new InsetDrawable(maskDrawable, padding, padding, padding, padding); + return new RippleDrawable( + MaterialColors.getColorStateList( + context, R.attr.colorControlHighlight, ColorStateList.valueOf(Color.TRANSPARENT)), + null, + maskWithPaddings); + } + } } diff --git a/lib/java/com/google/android/material/textfield/EndCompoundLayout.java b/lib/java/com/google/android/material/textfield/EndCompoundLayout.java index 7173b8ef1a8..f895bcae8f4 100644 --- a/lib/java/com/google/android/material/textfield/EndCompoundLayout.java +++ b/lib/java/com/google/android/material/textfield/EndCompoundLayout.java @@ -20,6 +20,7 @@ import static com.google.android.material.textfield.IconHelper.applyIconTint; import static com.google.android.material.textfield.IconHelper.refreshIconDrawableState; +import static com.google.android.material.textfield.IconHelper.setCompatRippleBackgroundIfNeeded; import static com.google.android.material.textfield.IconHelper.setIconOnClickListener; import static com.google.android.material.textfield.IconHelper.setIconOnLongClickListener; import static com.google.android.material.textfield.TextInputLayout.END_ICON_CLEAR_TEXT; @@ -177,6 +178,7 @@ private CheckableImageButton createIconView( (CheckableImageButton) inflater.inflate( R.layout.design_text_input_end_icon, root, false); iconView.setId(id); + setCompatRippleBackgroundIfNeeded(iconView); if (MaterialResources.isFontScaleAtLeast1_3(getContext())) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) iconView.getLayoutParams(); diff --git a/lib/java/com/google/android/material/textfield/IconHelper.java b/lib/java/com/google/android/material/textfield/IconHelper.java index 23f3bb68e79..41bcbc72194 100644 --- a/lib/java/com/google/android/material/textfield/IconHelper.java +++ b/lib/java/com/google/android/material/textfield/IconHelper.java @@ -16,9 +16,13 @@ package com.google.android.material.textfield; +import static com.google.android.material.internal.ViewUtils.dpToPx; + import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import androidx.annotation.NonNull; @@ -26,6 +30,7 @@ import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.ViewCompat; import com.google.android.material.internal.CheckableImageButton; +import com.google.android.material.ripple.RippleUtils; import java.util.Arrays; class IconHelper { @@ -126,4 +131,15 @@ private static int[] mergeIconState( return states; } + + static void setCompatRippleBackgroundIfNeeded(@NonNull CheckableImageButton iconView) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP + && VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) { + // Note that this is aligned with ?attr/actionBarItemBackground on API 23+, which sets ripple + // radius to 20dp. Therefore we set the padding here to (48dp [view size] - 20dp * 2) / 2. + iconView.setBackground( + RippleUtils.createOvalRippleLollipop( + iconView.getContext(), (int) dpToPx(iconView.getContext(), 4))); + } + } } diff --git a/lib/java/com/google/android/material/textfield/StartCompoundLayout.java b/lib/java/com/google/android/material/textfield/StartCompoundLayout.java index 8a35edf8b5f..84a85e7d214 100644 --- a/lib/java/com/google/android/material/textfield/StartCompoundLayout.java +++ b/lib/java/com/google/android/material/textfield/StartCompoundLayout.java @@ -20,6 +20,7 @@ import static com.google.android.material.textfield.IconHelper.applyIconTint; import static com.google.android.material.textfield.IconHelper.refreshIconDrawableState; +import static com.google.android.material.textfield.IconHelper.setCompatRippleBackgroundIfNeeded; import static com.google.android.material.textfield.IconHelper.setIconOnClickListener; import static com.google.android.material.textfield.IconHelper.setIconOnLongClickListener; @@ -84,6 +85,7 @@ class StartCompoundLayout extends LinearLayout { startIconView = (CheckableImageButton) layoutInflater.inflate(R.layout.design_text_input_start_icon, this, false); + setCompatRippleBackgroundIfNeeded(startIconView); prefixTextView = new AppCompatTextView(getContext());