Skip to content

Commit

Permalink
[TextField] Fix icon ripples go behind the edit text
Browse files Browse the repository at this point in the history
On API 21 & 22, the borderless ripple will go behind the container view under certain conditions. Setting a mask when creating the ripple drawable to restrain the ripple inside view bounds somehow solves the issue.

PiperOrigin-RevId: 445920587
  • Loading branch information
drchen authored and dsn5ft committed May 2, 2022
1 parent 61cbb8c commit 2c0e42f
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 0 deletions.
27 changes: 27 additions & 0 deletions lib/java/com/google/android/material/color/MaterialColors.java
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions lib/java/com/google/android/material/ripple/RippleUtils.java
Expand Up @@ -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.
Expand All @@ -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 = {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
16 changes: 16 additions & 0 deletions lib/java/com/google/android/material/textfield/IconHelper.java
Expand Up @@ -16,16 +16,21 @@

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;
import androidx.annotation.Nullable;
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 {
Expand Down Expand Up @@ -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)));
}
}
}
Expand Up @@ -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;

Expand Down Expand Up @@ -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());

Expand Down

0 comments on commit 2c0e42f

Please sign in to comment.