Skip to content

Commit 3b257c0

Browse files
drchenafohrman
authored andcommittedMay 19, 2022
[Switch] Support tinting thumb & track according to thumb position
PiperOrigin-RevId: 449760154
1 parent 2db8b0c commit 3b257c0

File tree

1 file changed

+140
-8
lines changed

1 file changed

+140
-8
lines changed
 

‎lib/java/com/google/android/material/materialswitch/MaterialSwitch.java

+140-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.android.material.R;
2020

21+
import static androidx.core.graphics.ColorUtils.blendARGB;
2122
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
2223

2324
import android.annotation.SuppressLint;
@@ -27,7 +28,10 @@
2728
import android.graphics.PorterDuff.Mode;
2829
import android.graphics.drawable.Drawable;
2930
import android.graphics.drawable.LayerDrawable;
31+
import android.os.Build.VERSION;
32+
import android.os.Build.VERSION_CODES;
3033
import androidx.appcompat.content.res.AppCompatResources;
34+
import android.support.v7.graphics.drawable.AnimatedStateListDrawableCompat;
3135
import androidx.appcompat.widget.DrawableUtils;
3236
import androidx.appcompat.widget.SwitchCompat;
3337
import androidx.appcompat.widget.TintTypedArray;
@@ -39,6 +43,7 @@
3943
import com.google.android.material.internal.ThemeEnforcement;
4044
import com.google.android.material.internal.ViewUtils;
4145
import java.lang.reflect.Field;
46+
import java.util.Arrays;
4247

4348
/**
4449
* A class that creates a Material Themed Switch. This class is intended to provide a brand new
@@ -54,6 +59,7 @@ public class MaterialSwitch extends SwitchCompat {
5459
@Nullable private Drawable trackDrawable;
5560
@Nullable private Drawable trackDecorationDrawable;
5661

62+
@Nullable private ColorStateList thumbTintList;
5763
@Nullable private ColorStateList trackTintList;
5864
@Nullable private ColorStateList trackDecorationTintList;
5965
@NonNull private PorterDuff.Mode trackDecorationTintMode;
@@ -71,6 +77,9 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in
7177
// Ensure we are using the correctly themed context rather than the context that was passed in.
7278
context = getContext();
7379

80+
thumbTintList = super.getThumbTintList();
81+
super.setThumbTintList(null); // Always use our custom tinting logic
82+
7483
trackDrawable = super.getTrackDrawable();
7584
trackTintList = super.getTrackTintList();
7685
super.setTrackTintList(null); // Always use our custom tinting logic
@@ -100,6 +109,16 @@ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
100109
switchWidth.set(getSwitchMinWidth());
101110
}
102111

112+
@Override
113+
public void invalidate() {
114+
// ThumbPosition update will trigger invalidate(), update thumb/track tint here.
115+
if (thumbPosition != null) {
116+
// This may happen when super classes' constructors call this method.
117+
updateDrawableTints();
118+
}
119+
super.invalidate();
120+
}
121+
103122
// TODO(b/227338106): remove this workaround and move to use setEnforceSwitchWidth(false) after
104123
// AppCompat 1.6.0-stable is released.
105124
@Override
@@ -126,6 +145,18 @@ public int getCompoundPaddingRight() {
126145
return super.getCompoundPaddingRight() - switchWidth.get() + getSwitchMinWidth();
127146
}
128147

148+
@Override
149+
public void setThumbTintList(@Nullable ColorStateList tint) {
150+
thumbTintList = tint;
151+
invalidate();
152+
}
153+
154+
@Override
155+
@Nullable
156+
public ColorStateList getThumbTintList() {
157+
return thumbTintList;
158+
}
159+
129160
@Override
130161
public void setTrackDrawable(@Nullable Drawable track) {
131162
trackDrawable = track;
@@ -247,9 +278,13 @@ private float getThumbPosition() {
247278
}
248279

249280
private void refreshTrackDrawable() {
250-
trackDrawable = setDrawableTintListIfNeeded(trackDrawable, trackTintList, getTrackTintMode());
251-
trackDecorationDrawable = setDrawableTintListIfNeeded(
252-
trackDecorationDrawable, trackDecorationTintList, trackDecorationTintMode);
281+
trackDrawable =
282+
createTintableDrawableIfNeeded(trackDrawable, trackTintList, getTrackTintMode());
283+
trackDecorationDrawable =
284+
createTintableDrawableIfNeeded(
285+
trackDecorationDrawable, trackDecorationTintList, trackDecorationTintMode);
286+
287+
updateDrawableTints();
253288

254289
Drawable finalTrackDrawable;
255290
if (trackDrawable != null && trackDecorationDrawable != null) {
@@ -266,17 +301,114 @@ private void refreshTrackDrawable() {
266301
super.setTrackDrawable(finalTrackDrawable);
267302
}
268303

269-
private static Drawable setDrawableTintListIfNeeded(
304+
private void updateDrawableTints() {
305+
if (thumbTintList == null && trackTintList == null && trackDecorationTintList == null) {
306+
// Early return to avoid heavy operation.
307+
return;
308+
}
309+
310+
float thumbPosition = getThumbPosition();
311+
312+
int[] currentState = getDrawableState();
313+
int[] currentStateUnchecked = getUncheckedState(currentState);
314+
int[] currentStateChecked = getCheckedState(currentState);
315+
316+
if (trackTintList != null) {
317+
setInterpolatedDrawableTintIfPossible(
318+
trackDrawable, trackTintList, currentStateUnchecked, currentStateChecked, thumbPosition);
319+
}
320+
321+
if (trackDecorationTintList != null) {
322+
setInterpolatedDrawableTintIfPossible(
323+
trackDecorationDrawable,
324+
trackDecorationTintList,
325+
currentStateUnchecked,
326+
currentStateChecked,
327+
thumbPosition);
328+
}
329+
330+
if (thumbTintList != null) {
331+
setInterpolatedDrawableTintIfPossible(
332+
getThumbDrawable(),
333+
thumbTintList,
334+
currentStateUnchecked,
335+
currentStateChecked,
336+
thumbPosition);
337+
}
338+
}
339+
340+
/** Returns a new state that removes the checked state from the input state. */
341+
private static int[] getUncheckedState(int[] state) {
342+
int[] newState = new int[state.length];
343+
int i = 0;
344+
for (int subState : state) {
345+
if (subState != android.R.attr.state_checked) {
346+
newState[i++] = subState;
347+
}
348+
}
349+
return newState;
350+
}
351+
352+
/** Returns a new state that adds the checked state to the input state. */
353+
private static int[] getCheckedState(int[] state) {
354+
for (int i = 0; i < state.length; i++) {
355+
if (state[i] == android.R.attr.state_checked) {
356+
return state;
357+
} else if (state[i] == 0) {
358+
int[] newState = state.clone();
359+
newState[i] = android.R.attr.state_checked;
360+
return newState;
361+
}
362+
}
363+
int[] newState = Arrays.copyOf(state, state.length + 1);
364+
newState[state.length] = android.R.attr.state_checked;
365+
return newState;
366+
}
367+
368+
/**
369+
* Tints the given drawable with the interpolated color according to the provided thumb position
370+
* between unchecked and checked states. The reference color in unchecked and checked states will
371+
* be retrieved from the given {@link ColorStateList} according to the provided states.
372+
*/
373+
private static void setInterpolatedDrawableTintIfPossible(
374+
@Nullable Drawable drawable,
375+
@Nullable ColorStateList tint,
376+
@NonNull int[] stateUnchecked,
377+
@NonNull int[] stateChecked,
378+
float thumbPosition) {
379+
if (drawable == null || tint == null) {
380+
return;
381+
}
382+
// TODO(b/232529333): remove this workaround after updating AppCompat version to 1.6.
383+
if (drawable instanceof AnimatedStateListDrawableCompat
384+
&& VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
385+
DrawableCompat.setTintList(
386+
drawable,
387+
ColorStateList.valueOf(
388+
blendARGB(
389+
tint.getColorForState(stateUnchecked, 0),
390+
tint.getColorForState(stateChecked, 0),
391+
thumbPosition)));
392+
return;
393+
}
394+
DrawableCompat.setTint(
395+
drawable,
396+
blendARGB(
397+
tint.getColorForState(stateUnchecked, 0),
398+
tint.getColorForState(stateChecked, 0),
399+
thumbPosition));
400+
}
401+
402+
private static Drawable createTintableDrawableIfNeeded(
270403
Drawable drawable, ColorStateList tintList, Mode tintMode) {
271404
if (drawable == null) {
272405
return null;
273406
}
274407
if (tintList != null) {
275408
drawable = DrawableCompat.wrap(drawable).mutate();
276-
}
277-
DrawableCompat.setTintList(drawable, tintList);
278-
if (tintList != null && tintMode != null) {
279-
DrawableCompat.setTintMode(drawable, tintMode);
409+
if (tintMode != null) {
410+
DrawableCompat.setTintMode(drawable, tintMode);
411+
}
280412
}
281413
return drawable;
282414
}

0 commit comments

Comments
 (0)
Please sign in to comment.