18
18
19
19
import com .google .android .material .R ;
20
20
21
+ import static androidx .core .graphics .ColorUtils .blendARGB ;
21
22
import static com .google .android .material .theme .overlay .MaterialThemeOverlay .wrap ;
22
23
23
24
import android .annotation .SuppressLint ;
27
28
import android .graphics .PorterDuff .Mode ;
28
29
import android .graphics .drawable .Drawable ;
29
30
import android .graphics .drawable .LayerDrawable ;
31
+ import android .os .Build .VERSION ;
32
+ import android .os .Build .VERSION_CODES ;
30
33
import androidx .appcompat .content .res .AppCompatResources ;
34
+ import android .support .v7 .graphics .drawable .AnimatedStateListDrawableCompat ;
31
35
import androidx .appcompat .widget .DrawableUtils ;
32
36
import androidx .appcompat .widget .SwitchCompat ;
33
37
import androidx .appcompat .widget .TintTypedArray ;
39
43
import com .google .android .material .internal .ThemeEnforcement ;
40
44
import com .google .android .material .internal .ViewUtils ;
41
45
import java .lang .reflect .Field ;
46
+ import java .util .Arrays ;
42
47
43
48
/**
44
49
* 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 {
54
59
@ Nullable private Drawable trackDrawable ;
55
60
@ Nullable private Drawable trackDecorationDrawable ;
56
61
62
+ @ Nullable private ColorStateList thumbTintList ;
57
63
@ Nullable private ColorStateList trackTintList ;
58
64
@ Nullable private ColorStateList trackDecorationTintList ;
59
65
@ NonNull private PorterDuff .Mode trackDecorationTintMode ;
@@ -71,6 +77,9 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in
71
77
// Ensure we are using the correctly themed context rather than the context that was passed in.
72
78
context = getContext ();
73
79
80
+ thumbTintList = super .getThumbTintList ();
81
+ super .setThumbTintList (null ); // Always use our custom tinting logic
82
+
74
83
trackDrawable = super .getTrackDrawable ();
75
84
trackTintList = super .getTrackTintList ();
76
85
super .setTrackTintList (null ); // Always use our custom tinting logic
@@ -100,6 +109,16 @@ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
100
109
switchWidth .set (getSwitchMinWidth ());
101
110
}
102
111
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
+
103
122
// TODO(b/227338106): remove this workaround and move to use setEnforceSwitchWidth(false) after
104
123
// AppCompat 1.6.0-stable is released.
105
124
@ Override
@@ -126,6 +145,18 @@ public int getCompoundPaddingRight() {
126
145
return super .getCompoundPaddingRight () - switchWidth .get () + getSwitchMinWidth ();
127
146
}
128
147
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
+
129
160
@ Override
130
161
public void setTrackDrawable (@ Nullable Drawable track ) {
131
162
trackDrawable = track ;
@@ -247,9 +278,13 @@ private float getThumbPosition() {
247
278
}
248
279
249
280
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 ();
253
288
254
289
Drawable finalTrackDrawable ;
255
290
if (trackDrawable != null && trackDecorationDrawable != null ) {
@@ -266,17 +301,114 @@ private void refreshTrackDrawable() {
266
301
super .setTrackDrawable (finalTrackDrawable );
267
302
}
268
303
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 (
270
403
Drawable drawable , ColorStateList tintList , Mode tintMode ) {
271
404
if (drawable == null ) {
272
405
return null ;
273
406
}
274
407
if (tintList != null ) {
275
408
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
+ }
280
412
}
281
413
return drawable ;
282
414
}
0 commit comments