From a3ca744776aab9d69234e8ea52796feb3084c4df Mon Sep 17 00:00:00 2001 From: conradchen Date: Fri, 20 May 2022 15:16:56 -0400 Subject: [PATCH] [Switch] Create icon style for the new switch design PiperOrigin-RevId: 450037491 --- .../materialswitch/MaterialSwitch.java | 228 ++++++++++++++++-- .../res/drawable/mtrl_switch_thumb.xml | 19 +- .../materialswitch/res/values/attrs.xml | 64 ++++- 3 files changed, 281 insertions(+), 30 deletions(-) diff --git a/lib/java/com/google/android/material/materialswitch/MaterialSwitch.java b/lib/java/com/google/android/material/materialswitch/MaterialSwitch.java index 9ed6113c076..c76666770fc 100644 --- a/lib/java/com/google/android/material/materialswitch/MaterialSwitch.java +++ b/lib/java/com/google/android/material/materialswitch/MaterialSwitch.java @@ -36,6 +36,7 @@ import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.widget.TintTypedArray; import android.util.AttributeSet; +import android.view.Gravity; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -52,14 +53,20 @@ */ public class MaterialSwitch extends SwitchCompat { private static final int DEF_STYLE_RES = R.style.Widget_Material3_CompoundButton_MaterialSwitch; + private static final int[] STATE_SET_WITH_ICON = { R.attr.state_with_icon }; @NonNull private final SwitchWidth switchWidth = SwitchWidth.create(this); @NonNull private final ThumbPosition thumbPosition = new ThumbPosition(); + @Nullable private Drawable thumbDrawable; + @Nullable private Drawable thumbIconDrawable; + @Nullable private Drawable trackDrawable; @Nullable private Drawable trackDecorationDrawable; @Nullable private ColorStateList thumbTintList; + @Nullable private ColorStateList thumbIconTintList; + @NonNull private PorterDuff.Mode thumbIconTintMode; @Nullable private ColorStateList trackTintList; @Nullable private ColorStateList trackDecorationTintList; @NonNull private PorterDuff.Mode trackDecorationTintMode; @@ -77,6 +84,7 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in // Ensure we are using the correctly themed context rather than the context that was passed in. context = getContext(); + thumbDrawable = super.getThumbDrawable(); thumbTintList = super.getThumbTintList(); super.setThumbTintList(null); // Always use our custom tinting logic @@ -88,6 +96,12 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in ThemeEnforcement.obtainTintedStyledAttributes( context, attrs, R.styleable.MaterialSwitch, defStyleAttr, DEF_STYLE_RES); + thumbIconDrawable = attributes.getDrawable(R.styleable.MaterialSwitch_thumbIcon); + thumbIconTintList = attributes.getColorStateList(R.styleable.MaterialSwitch_thumbIconTint); + thumbIconTintMode = + DrawableUtils.parseTintMode( + attributes.getInt(R.styleable.MaterialSwitch_thumbIconTintMode, -1), Mode.SRC_IN); + trackDecorationDrawable = attributes.getDrawable(R.styleable.MaterialSwitch_trackDecoration); trackDecorationTintList = @@ -98,6 +112,7 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in attributes.recycle(); + refreshThumbDrawable(); refreshTrackDrawable(); } @@ -119,6 +134,17 @@ public void invalidate() { super.invalidate(); } + @Override + protected int[] onCreateDrawableState(int extraSpace) { + int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (thumbIconDrawable != null) { + mergeDrawableStates(drawableState, STATE_SET_WITH_ICON); + } + + return drawableState; + } + // TODO(b/227338106): remove this workaround and move to use setEnforceSwitchWidth(false) after // AppCompat 1.6.0-stable is released. @Override @@ -146,9 +172,21 @@ public int getCompoundPaddingRight() { } @Override - public void setThumbTintList(@Nullable ColorStateList tint) { - thumbTintList = tint; - invalidate(); + public void setThumbDrawable(@Nullable Drawable drawable) { + thumbDrawable = drawable; + refreshThumbDrawable(); + } + + @Override + @Nullable + public Drawable getThumbDrawable() { + return thumbDrawable; + } + + @Override + public void setThumbTintList(@Nullable ColorStateList tintList) { + thumbTintList = tintList; + refreshThumbDrawable(); } @Override @@ -157,6 +195,96 @@ public ColorStateList getThumbTintList() { return thumbTintList; } + @Override + public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { + super.setThumbTintMode(tintMode); + refreshThumbDrawable(); + } + + /** + * Sets the drawable used for the thumb icon that will be drawn upon the thumb. + * + * @param resId Resource ID of a thumb icon drawable + * + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIcon + */ + public void setThumbIconResource(@DrawableRes int resId) { + setThumbIconDrawable(AppCompatResources.getDrawable(getContext(), resId)); + } + + /** + * Sets the drawable used for the thumb icon that will be drawn upon the thumb. + * + * @param icon Thumb icon drawable + * + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIcon + */ + public void setThumbIconDrawable(@Nullable Drawable icon) { + thumbIconDrawable = icon; + refreshThumbDrawable(); + } + + /** + * Gets the drawable used for the thumb icon that will be drawn upon the thumb. + * + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIcon + */ + @Nullable + public Drawable getThumbIconDrawable() { + return thumbIconDrawable; + } + + /** + * Applies a tint to the thumb icon drawable. Does not modify the current + * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + *

+ * Subsequent calls to {@link #setThumbIconDrawable(Drawable)} will + * automatically mutate the drawable and apply the specified tint and tint + * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}. + * + * @param tintList the tint to apply, may be {@code null} to clear tint + * + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTint + */ + public void setThumbIconTintList(@Nullable ColorStateList tintList) { + thumbIconTintList = tintList; + refreshThumbDrawable(); + } + + /** + * Returns the tint applied to the thumb icon drawable + * + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTint + */ + @Nullable + public ColorStateList getThumbIconTintList() { + return thumbIconTintList; + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setThumbIconTintList(ColorStateList)}} to the thumb icon drawable. + * The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint + + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTintMode + */ + public void setThumbIconTintMode(@NonNull PorterDuff.Mode tintMode) { + thumbIconTintMode = tintMode; + refreshThumbDrawable(); + } + + /** + * Returns the blending mode used to apply the tint to the thumb icon drawable + * + * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTintMode + */ + @NonNull + public PorterDuff.Mode getThumbIconTintMode() { + return thumbIconTintMode; + } + @Override public void setTrackDrawable(@Nullable Drawable track) { trackDrawable = track; @@ -170,8 +298,8 @@ public Drawable getTrackDrawable() { } @Override - public void setTrackTintList(@Nullable ColorStateList tint) { - trackTintList = tint; + public void setTrackTintList(@Nullable ColorStateList tintList) { + trackTintList = tintList; refreshTrackDrawable(); } @@ -228,12 +356,12 @@ public Drawable getTrackDecorationDrawable() { * automatically mutate the drawable and apply the specified tint and tint * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}. * - * @param tint the tint to apply, may be {@code null} to clear tint + * @param tintList the tint to apply, may be {@code null} to clear tint * * @attr ref com.google.android.material.R.styleable#MaterialSwitch_trackDecorationTint */ - public void setTrackDecorationTintList(@Nullable ColorStateList tint) { - trackDecorationTintList = tint; + public void setTrackDecorationTintList(@Nullable ColorStateList tintList) { + trackDecorationTintList = tintList; refreshTrackDrawable(); } @@ -277,6 +405,64 @@ private float getThumbPos() { return thumbPosition.get(); } + private void refreshThumbDrawable() { + thumbDrawable = + createTintableDrawableIfNeeded(thumbDrawable, thumbTintList, getThumbTintMode()); + thumbIconDrawable = + createTintableDrawableIfNeeded(thumbIconDrawable, thumbIconTintList, thumbIconTintMode); + + updateDrawableTints(); + + super.setThumbDrawable(compositeThumbAndIconDrawable(thumbDrawable, thumbIconDrawable)); + + refreshDrawableState(); + } + + @Nullable + private static Drawable compositeThumbAndIconDrawable( + @Nullable Drawable thumbDrawable, @Nullable Drawable thumbIconDrawable) { + if (thumbDrawable == null) { + return thumbIconDrawable; + } + if (thumbIconDrawable == null) { + return thumbDrawable; + } + LayerDrawable drawable = new LayerDrawable(new Drawable[]{thumbDrawable, thumbIconDrawable}); + int iconNewWidth; + int iconNewHeight; + if (thumbIconDrawable.getIntrinsicWidth() <= thumbDrawable.getIntrinsicWidth() + && thumbIconDrawable.getIntrinsicHeight() <= thumbDrawable.getIntrinsicHeight()) { + // If the icon is smaller than the thumb in both its width and height, keep icon's size. + iconNewWidth = thumbIconDrawable.getIntrinsicWidth(); + iconNewHeight = thumbIconDrawable.getIntrinsicHeight(); + } else { + float thumbIconRatio = + (float) thumbIconDrawable.getIntrinsicWidth() / thumbIconDrawable.getIntrinsicHeight(); + float thumbRatio = + (float) thumbDrawable.getIntrinsicWidth() / thumbDrawable.getIntrinsicHeight(); + if (thumbIconRatio >= thumbRatio) { + // If the icon is wider in ratio than the thumb, shrink it according to its width. + iconNewWidth = thumbDrawable.getIntrinsicWidth(); + iconNewHeight = (int) (iconNewWidth / thumbIconRatio); + } else { + // If the icon is taller in ratio than the thumb, shrink it according to its height. + iconNewHeight = thumbDrawable.getIntrinsicHeight(); + iconNewWidth = (int) (thumbIconRatio * iconNewHeight); + } + } + // Centers the icon inside the thumb. Before M there's no layer gravity support, we need to use + // layer insets to adjust the icon position manually. + if (VERSION.SDK_INT >= VERSION_CODES.M) { + drawable.setLayerSize(1, iconNewWidth, iconNewHeight); + drawable.setLayerGravity(1, Gravity.CENTER); + } else { + int horizontalInset = (thumbDrawable.getIntrinsicWidth() - iconNewWidth) / 2; + int verticalInset = (thumbDrawable.getIntrinsicHeight() - iconNewHeight) / 2; + drawable.setLayerInset(1, horizontalInset, verticalInset, horizontalInset, verticalInset); + } + return drawable; + } + private void refreshTrackDrawable() { trackDrawable = createTintableDrawableIfNeeded(trackDrawable, trackTintList, getTrackTintMode()); @@ -302,7 +488,10 @@ private void refreshTrackDrawable() { } private void updateDrawableTints() { - if (thumbTintList == null && trackTintList == null && trackDecorationTintList == null) { + if (thumbTintList == null + && thumbIconTintList == null + && trackTintList == null + && trackDecorationTintList == null) { // Early return to avoid heavy operation. return; } @@ -313,24 +502,29 @@ private void updateDrawableTints() { int[] currentStateUnchecked = getUncheckedState(currentState); int[] currentStateChecked = getCheckedState(currentState); - if (trackTintList != null) { + if (thumbTintList != null) { setInterpolatedDrawableTintIfPossible( - trackDrawable, trackTintList, currentStateUnchecked, currentStateChecked, thumbPosition); + thumbDrawable, thumbTintList, currentStateUnchecked, currentStateChecked, thumbPosition); } - if (trackDecorationTintList != null) { + if (thumbIconTintList != null) { setInterpolatedDrawableTintIfPossible( - trackDecorationDrawable, - trackDecorationTintList, + thumbIconDrawable, + thumbIconTintList, currentStateUnchecked, currentStateChecked, thumbPosition); } - if (thumbTintList != null) { + if (trackTintList != null) { + setInterpolatedDrawableTintIfPossible( + trackDrawable, trackTintList, currentStateUnchecked, currentStateChecked, thumbPosition); + } + + if (trackDecorationTintList != null) { setInterpolatedDrawableTintIfPossible( - getThumbDrawable(), - thumbTintList, + trackDecorationDrawable, + trackDecorationTintList, currentStateUnchecked, currentStateChecked, thumbPosition); diff --git a/lib/java/com/google/android/material/materialswitch/res/drawable/mtrl_switch_thumb.xml b/lib/java/com/google/android/material/materialswitch/res/drawable/mtrl_switch_thumb.xml index 56a09b187b5..1a41f6c033a 100644 --- a/lib/java/com/google/android/material/materialswitch/res/drawable/mtrl_switch_thumb.xml +++ b/lib/java/com/google/android/material/materialswitch/res/drawable/mtrl_switch_thumb.xml @@ -14,8 +14,8 @@ limitations under the License. --> - + + @@ -40,6 +45,11 @@ android:toId="@+id/checked" android:drawable="@drawable/mtrl_switch_thumb_pressed_checked" /> + + + + - - - - - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +