Skip to content

Commit

Permalink
[Switch] Create icon style for the new switch design
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 450037491
  • Loading branch information
drchen authored and afohrman committed May 23, 2022
1 parent 30bb583 commit a3ca744
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 30 deletions.
228 changes: 211 additions & 17 deletions lib/java/com/google/android/material/materialswitch/MaterialSwitch.java
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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

Expand All @@ -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 =
Expand All @@ -98,6 +112,7 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in

attributes.recycle();

refreshThumbDrawable();
refreshTrackDrawable();
}

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
* <p>
* 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;
Expand All @@ -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();
}

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

Expand Down Expand Up @@ -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());
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down
Expand Up @@ -14,8 +14,8 @@
limitations under the License.
-->

<animated-selector
xmlns:android="http://schemas.android.com/apk/res/android"
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:width="@dimen/mtrl_switch_thumb_size"
android:height="@dimen/mtrl_switch_thumb_size"
Expand All @@ -31,6 +31,11 @@
android:drawable="@drawable/mtrl_switch_thumb_checked"
android:state_checked="true" />

<item
android:id="@+id/with_icon"
android:drawable="@drawable/mtrl_switch_thumb_checked"
app:state_with_icon="true" />

<item
android:id="@+id/unchecked"
android:drawable="@drawable/mtrl_switch_thumb_unchecked" />
Expand All @@ -40,6 +45,11 @@
android:toId="@+id/checked"
android:drawable="@drawable/mtrl_switch_thumb_pressed_checked" />

<transition
android:fromId="@+id/pressed"
android:toId="@+id/with_icon"
android:drawable="@drawable/mtrl_switch_thumb_pressed_checked" />

<transition
android:fromId="@+id/pressed"
android:toId="@+id/unchecked"
Expand All @@ -55,6 +65,11 @@
android:toId="@+id/unchecked"
android:drawable="@drawable/mtrl_switch_thumb_checked_unchecked" />

<transition
android:fromId="@+id/with_icon"
android:toId="@+id/pressed"
android:drawable="@drawable/mtrl_switch_thumb_checked_pressed" />

<transition
android:fromId="@+id/unchecked"
android:toId="@+id/pressed"
Expand Down

0 comments on commit a3ca744

Please sign in to comment.