3636import androidx .appcompat .widget .SwitchCompat ;
3737import androidx .appcompat .widget .TintTypedArray ;
3838import android .util .AttributeSet ;
39+ import android .view .Gravity ;
3940import androidx .annotation .DrawableRes ;
4041import androidx .annotation .NonNull ;
4142import androidx .annotation .Nullable ;
5253 */
5354public class MaterialSwitch extends SwitchCompat {
5455 private static final int DEF_STYLE_RES = R .style .Widget_Material3_CompoundButton_MaterialSwitch ;
56+ private static final int [] STATE_SET_WITH_ICON = { R .attr .state_with_icon };
5557
5658 @ NonNull private final SwitchWidth switchWidth = SwitchWidth .create (this );
5759 @ NonNull private final ThumbPosition thumbPosition = new ThumbPosition ();
5860
61+ @ Nullable private Drawable thumbDrawable ;
62+ @ Nullable private Drawable thumbIconDrawable ;
63+
5964 @ Nullable private Drawable trackDrawable ;
6065 @ Nullable private Drawable trackDecorationDrawable ;
6166
6267 @ Nullable private ColorStateList thumbTintList ;
68+ @ Nullable private ColorStateList thumbIconTintList ;
69+ @ NonNull private PorterDuff .Mode thumbIconTintMode ;
6370 @ Nullable private ColorStateList trackTintList ;
6471 @ Nullable private ColorStateList trackDecorationTintList ;
6572 @ NonNull private PorterDuff .Mode trackDecorationTintMode ;
@@ -77,6 +84,7 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in
7784 // Ensure we are using the correctly themed context rather than the context that was passed in.
7885 context = getContext ();
7986
87+ thumbDrawable = super .getThumbDrawable ();
8088 thumbTintList = super .getThumbTintList ();
8189 super .setThumbTintList (null ); // Always use our custom tinting logic
8290
@@ -88,6 +96,12 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in
8896 ThemeEnforcement .obtainTintedStyledAttributes (
8997 context , attrs , R .styleable .MaterialSwitch , defStyleAttr , DEF_STYLE_RES );
9098
99+ thumbIconDrawable = attributes .getDrawable (R .styleable .MaterialSwitch_thumbIcon );
100+ thumbIconTintList = attributes .getColorStateList (R .styleable .MaterialSwitch_thumbIconTint );
101+ thumbIconTintMode =
102+ DrawableUtils .parseTintMode (
103+ attributes .getInt (R .styleable .MaterialSwitch_thumbIconTintMode , -1 ), Mode .SRC_IN );
104+
91105 trackDecorationDrawable =
92106 attributes .getDrawable (R .styleable .MaterialSwitch_trackDecoration );
93107 trackDecorationTintList =
@@ -98,6 +112,7 @@ public MaterialSwitch(@NonNull Context context, @Nullable AttributeSet attrs, in
98112
99113 attributes .recycle ();
100114
115+ refreshThumbDrawable ();
101116 refreshTrackDrawable ();
102117 }
103118
@@ -119,6 +134,17 @@ public void invalidate() {
119134 super .invalidate ();
120135 }
121136
137+ @ Override
138+ protected int [] onCreateDrawableState (int extraSpace ) {
139+ int [] drawableState = super .onCreateDrawableState (extraSpace + 1 );
140+
141+ if (thumbIconDrawable != null ) {
142+ mergeDrawableStates (drawableState , STATE_SET_WITH_ICON );
143+ }
144+
145+ return drawableState ;
146+ }
147+
122148 // TODO(b/227338106): remove this workaround and move to use setEnforceSwitchWidth(false) after
123149 // AppCompat 1.6.0-stable is released.
124150 @ Override
@@ -146,9 +172,21 @@ public int getCompoundPaddingRight() {
146172 }
147173
148174 @ Override
149- public void setThumbTintList (@ Nullable ColorStateList tint ) {
150- thumbTintList = tint ;
151- invalidate ();
175+ public void setThumbDrawable (@ Nullable Drawable drawable ) {
176+ thumbDrawable = drawable ;
177+ refreshThumbDrawable ();
178+ }
179+
180+ @ Override
181+ @ Nullable
182+ public Drawable getThumbDrawable () {
183+ return thumbDrawable ;
184+ }
185+
186+ @ Override
187+ public void setThumbTintList (@ Nullable ColorStateList tintList ) {
188+ thumbTintList = tintList ;
189+ refreshThumbDrawable ();
152190 }
153191
154192 @ Override
@@ -157,6 +195,96 @@ public ColorStateList getThumbTintList() {
157195 return thumbTintList ;
158196 }
159197
198+ @ Override
199+ public void setThumbTintMode (@ Nullable PorterDuff .Mode tintMode ) {
200+ super .setThumbTintMode (tintMode );
201+ refreshThumbDrawable ();
202+ }
203+
204+ /**
205+ * Sets the drawable used for the thumb icon that will be drawn upon the thumb.
206+ *
207+ * @param resId Resource ID of a thumb icon drawable
208+ *
209+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIcon
210+ */
211+ public void setThumbIconResource (@ DrawableRes int resId ) {
212+ setThumbIconDrawable (AppCompatResources .getDrawable (getContext (), resId ));
213+ }
214+
215+ /**
216+ * Sets the drawable used for the thumb icon that will be drawn upon the thumb.
217+ *
218+ * @param icon Thumb icon drawable
219+ *
220+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIcon
221+ */
222+ public void setThumbIconDrawable (@ Nullable Drawable icon ) {
223+ thumbIconDrawable = icon ;
224+ refreshThumbDrawable ();
225+ }
226+
227+ /**
228+ * Gets the drawable used for the thumb icon that will be drawn upon the thumb.
229+ *
230+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIcon
231+ */
232+ @ Nullable
233+ public Drawable getThumbIconDrawable () {
234+ return thumbIconDrawable ;
235+ }
236+
237+ /**
238+ * Applies a tint to the thumb icon drawable. Does not modify the current
239+ * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
240+ * <p>
241+ * Subsequent calls to {@link #setThumbIconDrawable(Drawable)} will
242+ * automatically mutate the drawable and apply the specified tint and tint
243+ * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.
244+ *
245+ * @param tintList the tint to apply, may be {@code null} to clear tint
246+ *
247+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTint
248+ */
249+ public void setThumbIconTintList (@ Nullable ColorStateList tintList ) {
250+ thumbIconTintList = tintList ;
251+ refreshThumbDrawable ();
252+ }
253+
254+ /**
255+ * Returns the tint applied to the thumb icon drawable
256+ *
257+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTint
258+ */
259+ @ Nullable
260+ public ColorStateList getThumbIconTintList () {
261+ return thumbIconTintList ;
262+ }
263+
264+ /**
265+ * Specifies the blending mode used to apply the tint specified by
266+ * {@link #setThumbIconTintList(ColorStateList)}} to the thumb icon drawable.
267+ * The default mode is {@link PorterDuff.Mode#SRC_IN}.
268+ *
269+ * @param tintMode the blending mode used to apply the tint
270+
271+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTintMode
272+ */
273+ public void setThumbIconTintMode (@ NonNull PorterDuff .Mode tintMode ) {
274+ thumbIconTintMode = tintMode ;
275+ refreshThumbDrawable ();
276+ }
277+
278+ /**
279+ * Returns the blending mode used to apply the tint to the thumb icon drawable
280+ *
281+ * @attr ref com.google.android.material.R.styleable#MaterialSwitch_thumbIconTintMode
282+ */
283+ @ NonNull
284+ public PorterDuff .Mode getThumbIconTintMode () {
285+ return thumbIconTintMode ;
286+ }
287+
160288 @ Override
161289 public void setTrackDrawable (@ Nullable Drawable track ) {
162290 trackDrawable = track ;
@@ -170,8 +298,8 @@ public Drawable getTrackDrawable() {
170298 }
171299
172300 @ Override
173- public void setTrackTintList (@ Nullable ColorStateList tint ) {
174- trackTintList = tint ;
301+ public void setTrackTintList (@ Nullable ColorStateList tintList ) {
302+ trackTintList = tintList ;
175303 refreshTrackDrawable ();
176304 }
177305
@@ -228,12 +356,12 @@ public Drawable getTrackDecorationDrawable() {
228356 * automatically mutate the drawable and apply the specified tint and tint
229357 * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.
230358 *
231- * @param tint the tint to apply, may be {@code null} to clear tint
359+ * @param tintList the tint to apply, may be {@code null} to clear tint
232360 *
233361 * @attr ref com.google.android.material.R.styleable#MaterialSwitch_trackDecorationTint
234362 */
235- public void setTrackDecorationTintList (@ Nullable ColorStateList tint ) {
236- trackDecorationTintList = tint ;
363+ public void setTrackDecorationTintList (@ Nullable ColorStateList tintList ) {
364+ trackDecorationTintList = tintList ;
237365 refreshTrackDrawable ();
238366 }
239367
@@ -277,6 +405,64 @@ private float getThumbPos() {
277405 return thumbPosition .get ();
278406 }
279407
408+ private void refreshThumbDrawable () {
409+ thumbDrawable =
410+ createTintableDrawableIfNeeded (thumbDrawable , thumbTintList , getThumbTintMode ());
411+ thumbIconDrawable =
412+ createTintableDrawableIfNeeded (thumbIconDrawable , thumbIconTintList , thumbIconTintMode );
413+
414+ updateDrawableTints ();
415+
416+ super .setThumbDrawable (compositeThumbAndIconDrawable (thumbDrawable , thumbIconDrawable ));
417+
418+ refreshDrawableState ();
419+ }
420+
421+ @ Nullable
422+ private static Drawable compositeThumbAndIconDrawable (
423+ @ Nullable Drawable thumbDrawable , @ Nullable Drawable thumbIconDrawable ) {
424+ if (thumbDrawable == null ) {
425+ return thumbIconDrawable ;
426+ }
427+ if (thumbIconDrawable == null ) {
428+ return thumbDrawable ;
429+ }
430+ LayerDrawable drawable = new LayerDrawable (new Drawable []{thumbDrawable , thumbIconDrawable });
431+ int iconNewWidth ;
432+ int iconNewHeight ;
433+ if (thumbIconDrawable .getIntrinsicWidth () <= thumbDrawable .getIntrinsicWidth ()
434+ && thumbIconDrawable .getIntrinsicHeight () <= thumbDrawable .getIntrinsicHeight ()) {
435+ // If the icon is smaller than the thumb in both its width and height, keep icon's size.
436+ iconNewWidth = thumbIconDrawable .getIntrinsicWidth ();
437+ iconNewHeight = thumbIconDrawable .getIntrinsicHeight ();
438+ } else {
439+ float thumbIconRatio =
440+ (float ) thumbIconDrawable .getIntrinsicWidth () / thumbIconDrawable .getIntrinsicHeight ();
441+ float thumbRatio =
442+ (float ) thumbDrawable .getIntrinsicWidth () / thumbDrawable .getIntrinsicHeight ();
443+ if (thumbIconRatio >= thumbRatio ) {
444+ // If the icon is wider in ratio than the thumb, shrink it according to its width.
445+ iconNewWidth = thumbDrawable .getIntrinsicWidth ();
446+ iconNewHeight = (int ) (iconNewWidth / thumbIconRatio );
447+ } else {
448+ // If the icon is taller in ratio than the thumb, shrink it according to its height.
449+ iconNewHeight = thumbDrawable .getIntrinsicHeight ();
450+ iconNewWidth = (int ) (thumbIconRatio * iconNewHeight );
451+ }
452+ }
453+ // Centers the icon inside the thumb. Before M there's no layer gravity support, we need to use
454+ // layer insets to adjust the icon position manually.
455+ if (VERSION .SDK_INT >= VERSION_CODES .M ) {
456+ drawable .setLayerSize (1 , iconNewWidth , iconNewHeight );
457+ drawable .setLayerGravity (1 , Gravity .CENTER );
458+ } else {
459+ int horizontalInset = (thumbDrawable .getIntrinsicWidth () - iconNewWidth ) / 2 ;
460+ int verticalInset = (thumbDrawable .getIntrinsicHeight () - iconNewHeight ) / 2 ;
461+ drawable .setLayerInset (1 , horizontalInset , verticalInset , horizontalInset , verticalInset );
462+ }
463+ return drawable ;
464+ }
465+
280466 private void refreshTrackDrawable () {
281467 trackDrawable =
282468 createTintableDrawableIfNeeded (trackDrawable , trackTintList , getTrackTintMode ());
@@ -302,7 +488,10 @@ private void refreshTrackDrawable() {
302488 }
303489
304490 private void updateDrawableTints () {
305- if (thumbTintList == null && trackTintList == null && trackDecorationTintList == null ) {
491+ if (thumbTintList == null
492+ && thumbIconTintList == null
493+ && trackTintList == null
494+ && trackDecorationTintList == null ) {
306495 // Early return to avoid heavy operation.
307496 return ;
308497 }
@@ -313,24 +502,29 @@ private void updateDrawableTints() {
313502 int [] currentStateUnchecked = getUncheckedState (currentState );
314503 int [] currentStateChecked = getCheckedState (currentState );
315504
316- if (trackTintList != null ) {
505+ if (thumbTintList != null ) {
317506 setInterpolatedDrawableTintIfPossible (
318- trackDrawable , trackTintList , currentStateUnchecked , currentStateChecked , thumbPosition );
507+ thumbDrawable , thumbTintList , currentStateUnchecked , currentStateChecked , thumbPosition );
319508 }
320509
321- if (trackDecorationTintList != null ) {
510+ if (thumbIconTintList != null ) {
322511 setInterpolatedDrawableTintIfPossible (
323- trackDecorationDrawable ,
324- trackDecorationTintList ,
512+ thumbIconDrawable ,
513+ thumbIconTintList ,
325514 currentStateUnchecked ,
326515 currentStateChecked ,
327516 thumbPosition );
328517 }
329518
330- if (thumbTintList != null ) {
519+ if (trackTintList != null ) {
520+ setInterpolatedDrawableTintIfPossible (
521+ trackDrawable , trackTintList , currentStateUnchecked , currentStateChecked , thumbPosition );
522+ }
523+
524+ if (trackDecorationTintList != null ) {
331525 setInterpolatedDrawableTintIfPossible (
332- getThumbDrawable () ,
333- thumbTintList ,
526+ trackDecorationDrawable ,
527+ trackDecorationTintList ,
334528 currentStateUnchecked ,
335529 currentStateChecked ,
336530 thumbPosition );
0 commit comments