1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.animation.ObjectAnimator;
20 import android.annotation.DrawableRes;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.StyleRes;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.graphics.Canvas;
29 import android.graphics.Insets;
30 import android.graphics.Paint;
31 import android.graphics.PorterDuff;
32 import android.graphics.Rect;
33 import android.graphics.Region.Op;
34 import android.graphics.Typeface;
35 import android.graphics.drawable.Drawable;
36 import android.os.Build.VERSION_CODES;
37 import android.text.Layout;
38 import android.text.StaticLayout;
39 import android.text.TextPaint;
40 import android.text.TextUtils;
41 import android.text.method.AllCapsTransformationMethod;
42 import android.text.method.TransformationMethod2;
43 import android.util.AttributeSet;
44 import android.util.FloatProperty;
45 import android.util.MathUtils;
46 import android.view.Gravity;
47 import android.view.MotionEvent;
48 import android.view.SoundEffectConstants;
49 import android.view.VelocityTracker;
50 import android.view.ViewConfiguration;
51 import android.view.ViewStructure;
52 import android.view.accessibility.AccessibilityEvent;
53 import android.view.accessibility.AccessibilityNodeInfo;
54 
55 import com.android.internal.R;
56 
57 /**
58  * A Switch is a two-state toggle switch widget that can select between two
59  * options. The user may drag the "thumb" back and forth to choose the selected option,
60  * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
61  * property controls the text displayed in the label for the switch, whereas the
62  * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
63  * controls the text on the thumb. Similarly, the
64  * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
65  * setTypeface() methods control the typeface and style of label text, whereas the
66  * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
67  * the related setSwitchTypeface() methods control that of the thumb.
68  *
69  * <p>{@link android.support.v7.widget.SwitchCompat} is a version of
70  * the Switch widget which runs on devices back to API 7.</p>
71  *
72  * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
73  * guide.</p>
74  *
75  * @attr ref android.R.styleable#Switch_textOn
76  * @attr ref android.R.styleable#Switch_textOff
77  * @attr ref android.R.styleable#Switch_switchMinWidth
78  * @attr ref android.R.styleable#Switch_switchPadding
79  * @attr ref android.R.styleable#Switch_switchTextAppearance
80  * @attr ref android.R.styleable#Switch_thumb
81  * @attr ref android.R.styleable#Switch_thumbTextPadding
82  * @attr ref android.R.styleable#Switch_track
83  */
84 public class Switch extends CompoundButton {
85     private static final int THUMB_ANIMATION_DURATION = 250;
86 
87     private static final int TOUCH_MODE_IDLE = 0;
88     private static final int TOUCH_MODE_DOWN = 1;
89     private static final int TOUCH_MODE_DRAGGING = 2;
90 
91     // Enum for the "typeface" XML parameter.
92     private static final int SANS = 1;
93     private static final int SERIF = 2;
94     private static final int MONOSPACE = 3;
95 
96     private Drawable mThumbDrawable;
97     private ColorStateList mThumbTintList = null;
98     private PorterDuff.Mode mThumbTintMode = null;
99     private boolean mHasThumbTint = false;
100     private boolean mHasThumbTintMode = false;
101 
102     private Drawable mTrackDrawable;
103     private ColorStateList mTrackTintList = null;
104     private PorterDuff.Mode mTrackTintMode = null;
105     private boolean mHasTrackTint = false;
106     private boolean mHasTrackTintMode = false;
107 
108     private int mThumbTextPadding;
109     private int mSwitchMinWidth;
110     private int mSwitchPadding;
111     private boolean mSplitTrack;
112     private CharSequence mTextOn;
113     private CharSequence mTextOff;
114     private boolean mShowText;
115     private boolean mUseFallbackLineSpacing;
116 
117     private int mTouchMode;
118     private int mTouchSlop;
119     private float mTouchX;
120     private float mTouchY;
121     private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
122     private int mMinFlingVelocity;
123 
124     private float mThumbPosition;
125 
126     /**
127      * Width required to draw the switch track and thumb. Includes padding and
128      * optical bounds for both the track and thumb.
129      */
130     private int mSwitchWidth;
131 
132     /**
133      * Height required to draw the switch track and thumb. Includes padding and
134      * optical bounds for both the track and thumb.
135      */
136     private int mSwitchHeight;
137 
138     /**
139      * Width of the thumb's content region. Does not include padding or
140      * optical bounds.
141      */
142     private int mThumbWidth;
143 
144     /** Left bound for drawing the switch track and thumb. */
145     private int mSwitchLeft;
146 
147     /** Top bound for drawing the switch track and thumb. */
148     private int mSwitchTop;
149 
150     /** Right bound for drawing the switch track and thumb. */
151     private int mSwitchRight;
152 
153     /** Bottom bound for drawing the switch track and thumb. */
154     private int mSwitchBottom;
155 
156     private TextPaint mTextPaint;
157     private ColorStateList mTextColors;
158     private Layout mOnLayout;
159     private Layout mOffLayout;
160     private TransformationMethod2 mSwitchTransformationMethod;
161     private ObjectAnimator mPositionAnimator;
162 
163     @SuppressWarnings("hiding")
164     private final Rect mTempRect = new Rect();
165 
166     private static final int[] CHECKED_STATE_SET = {
167         R.attr.state_checked
168     };
169 
170     /**
171      * Construct a new Switch with default styling.
172      *
173      * @param context The Context that will determine this widget's theming.
174      */
Switch(Context context)175     public Switch(Context context) {
176         this(context, null);
177     }
178 
179     /**
180      * Construct a new Switch with default styling, overriding specific style
181      * attributes as requested.
182      *
183      * @param context The Context that will determine this widget's theming.
184      * @param attrs Specification of attributes that should deviate from default styling.
185      */
Switch(Context context, AttributeSet attrs)186     public Switch(Context context, AttributeSet attrs) {
187         this(context, attrs, com.android.internal.R.attr.switchStyle);
188     }
189 
190     /**
191      * Construct a new Switch with a default style determined by the given theme attribute,
192      * overriding specific style attributes as requested.
193      *
194      * @param context The Context that will determine this widget's theming.
195      * @param attrs Specification of attributes that should deviate from the default styling.
196      * @param defStyleAttr An attribute in the current theme that contains a
197      *        reference to a style resource that supplies default values for
198      *        the view. Can be 0 to not look for defaults.
199      */
Switch(Context context, AttributeSet attrs, int defStyleAttr)200     public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
201         this(context, attrs, defStyleAttr, 0);
202     }
203 
204 
205     /**
206      * Construct a new Switch with a default style determined by the given theme
207      * attribute or style resource, overriding specific style attributes as
208      * requested.
209      *
210      * @param context The Context that will determine this widget's theming.
211      * @param attrs Specification of attributes that should deviate from the
212      *        default styling.
213      * @param defStyleAttr An attribute in the current theme that contains a
214      *        reference to a style resource that supplies default values for
215      *        the view. Can be 0 to not look for defaults.
216      * @param defStyleRes A resource identifier of a style resource that
217      *        supplies default values for the view, used only if
218      *        defStyleAttr is 0 or can not be found in the theme. Can be 0
219      *        to not look for defaults.
220      */
Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)221     public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
222         super(context, attrs, defStyleAttr, defStyleRes);
223 
224         mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
225 
226         final Resources res = getResources();
227         mTextPaint.density = res.getDisplayMetrics().density;
228         mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
229 
230         final TypedArray a = context.obtainStyledAttributes(
231                 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
232         mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
233         if (mThumbDrawable != null) {
234             mThumbDrawable.setCallback(this);
235         }
236         mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
237         if (mTrackDrawable != null) {
238             mTrackDrawable.setCallback(this);
239         }
240         mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
241         mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
242         mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
243         mThumbTextPadding = a.getDimensionPixelSize(
244                 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
245         mSwitchMinWidth = a.getDimensionPixelSize(
246                 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
247         mSwitchPadding = a.getDimensionPixelSize(
248                 com.android.internal.R.styleable.Switch_switchPadding, 0);
249         mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
250 
251         mUseFallbackLineSpacing = context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.P;
252 
253         ColorStateList thumbTintList = a.getColorStateList(
254                 com.android.internal.R.styleable.Switch_thumbTint);
255         if (thumbTintList != null) {
256             mThumbTintList = thumbTintList;
257             mHasThumbTint = true;
258         }
259         PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
260                 a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
261         if (mThumbTintMode != thumbTintMode) {
262             mThumbTintMode = thumbTintMode;
263             mHasThumbTintMode = true;
264         }
265         if (mHasThumbTint || mHasThumbTintMode) {
266             applyThumbTint();
267         }
268 
269         ColorStateList trackTintList = a.getColorStateList(
270                 com.android.internal.R.styleable.Switch_trackTint);
271         if (trackTintList != null) {
272             mTrackTintList = trackTintList;
273             mHasTrackTint = true;
274         }
275         PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
276                 a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
277         if (mTrackTintMode != trackTintMode) {
278             mTrackTintMode = trackTintMode;
279             mHasTrackTintMode = true;
280         }
281         if (mHasTrackTint || mHasTrackTintMode) {
282             applyTrackTint();
283         }
284 
285         final int appearance = a.getResourceId(
286                 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
287         if (appearance != 0) {
288             setSwitchTextAppearance(context, appearance);
289         }
290         a.recycle();
291 
292         final ViewConfiguration config = ViewConfiguration.get(context);
293         mTouchSlop = config.getScaledTouchSlop();
294         mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
295 
296         // Refresh display with current params
297         refreshDrawableState();
298         setChecked(isChecked());
299     }
300 
301     /**
302      * Sets the switch text color, size, style, hint color, and highlight color
303      * from the specified TextAppearance resource.
304      *
305      * @attr ref android.R.styleable#Switch_switchTextAppearance
306      */
setSwitchTextAppearance(Context context, @StyleRes int resid)307     public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
308         TypedArray appearance =
309                 context.obtainStyledAttributes(resid,
310                         com.android.internal.R.styleable.TextAppearance);
311 
312         ColorStateList colors;
313         int ts;
314 
315         colors = appearance.getColorStateList(com.android.internal.R.styleable.
316                 TextAppearance_textColor);
317         if (colors != null) {
318             mTextColors = colors;
319         } else {
320             // If no color set in TextAppearance, default to the view's textColor
321             mTextColors = getTextColors();
322         }
323 
324         ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
325                 TextAppearance_textSize, 0);
326         if (ts != 0) {
327             if (ts != mTextPaint.getTextSize()) {
328                 mTextPaint.setTextSize(ts);
329                 requestLayout();
330             }
331         }
332 
333         int typefaceIndex, styleIndex;
334 
335         typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
336                 TextAppearance_typeface, -1);
337         styleIndex = appearance.getInt(com.android.internal.R.styleable.
338                 TextAppearance_textStyle, -1);
339 
340         setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
341 
342         boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
343                 TextAppearance_textAllCaps, false);
344         if (allCaps) {
345             mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
346             mSwitchTransformationMethod.setLengthChangesAllowed(true);
347         } else {
348             mSwitchTransformationMethod = null;
349         }
350 
351         appearance.recycle();
352     }
353 
setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex)354     private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
355         Typeface tf = null;
356         switch (typefaceIndex) {
357             case SANS:
358                 tf = Typeface.SANS_SERIF;
359                 break;
360 
361             case SERIF:
362                 tf = Typeface.SERIF;
363                 break;
364 
365             case MONOSPACE:
366                 tf = Typeface.MONOSPACE;
367                 break;
368         }
369 
370         setSwitchTypeface(tf, styleIndex);
371     }
372 
373     /**
374      * Sets the typeface and style in which the text should be displayed on the
375      * switch, and turns on the fake bold and italic bits in the Paint if the
376      * Typeface that you provided does not have all the bits in the
377      * style that you specified.
378      */
setSwitchTypeface(Typeface tf, int style)379     public void setSwitchTypeface(Typeface tf, int style) {
380         if (style > 0) {
381             if (tf == null) {
382                 tf = Typeface.defaultFromStyle(style);
383             } else {
384                 tf = Typeface.create(tf, style);
385             }
386 
387             setSwitchTypeface(tf);
388             // now compute what (if any) algorithmic styling is needed
389             int typefaceStyle = tf != null ? tf.getStyle() : 0;
390             int need = style & ~typefaceStyle;
391             mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
392             mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
393         } else {
394             mTextPaint.setFakeBoldText(false);
395             mTextPaint.setTextSkewX(0);
396             setSwitchTypeface(tf);
397         }
398     }
399 
400     /**
401      * Sets the typeface in which the text should be displayed on the switch.
402      * Note that not all Typeface families actually have bold and italic
403      * variants, so you may need to use
404      * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
405      * that you actually want.
406      *
407      * @attr ref android.R.styleable#TextView_typeface
408      * @attr ref android.R.styleable#TextView_textStyle
409      */
setSwitchTypeface(Typeface tf)410     public void setSwitchTypeface(Typeface tf) {
411         if (mTextPaint.getTypeface() != tf) {
412             mTextPaint.setTypeface(tf);
413 
414             requestLayout();
415             invalidate();
416         }
417     }
418 
419     /**
420      * Set the amount of horizontal padding between the switch and the associated text.
421      *
422      * @param pixels Amount of padding in pixels
423      *
424      * @attr ref android.R.styleable#Switch_switchPadding
425      */
setSwitchPadding(int pixels)426     public void setSwitchPadding(int pixels) {
427         mSwitchPadding = pixels;
428         requestLayout();
429     }
430 
431     /**
432      * Get the amount of horizontal padding between the switch and the associated text.
433      *
434      * @return Amount of padding in pixels
435      *
436      * @attr ref android.R.styleable#Switch_switchPadding
437      */
getSwitchPadding()438     public int getSwitchPadding() {
439         return mSwitchPadding;
440     }
441 
442     /**
443      * Set the minimum width of the switch in pixels. The switch's width will be the maximum
444      * of this value and its measured width as determined by the switch drawables and text used.
445      *
446      * @param pixels Minimum width of the switch in pixels
447      *
448      * @attr ref android.R.styleable#Switch_switchMinWidth
449      */
setSwitchMinWidth(int pixels)450     public void setSwitchMinWidth(int pixels) {
451         mSwitchMinWidth = pixels;
452         requestLayout();
453     }
454 
455     /**
456      * Get the minimum width of the switch in pixels. The switch's width will be the maximum
457      * of this value and its measured width as determined by the switch drawables and text used.
458      *
459      * @return Minimum width of the switch in pixels
460      *
461      * @attr ref android.R.styleable#Switch_switchMinWidth
462      */
getSwitchMinWidth()463     public int getSwitchMinWidth() {
464         return mSwitchMinWidth;
465     }
466 
467     /**
468      * Set the horizontal padding around the text drawn on the switch itself.
469      *
470      * @param pixels Horizontal padding for switch thumb text in pixels
471      *
472      * @attr ref android.R.styleable#Switch_thumbTextPadding
473      */
setThumbTextPadding(int pixels)474     public void setThumbTextPadding(int pixels) {
475         mThumbTextPadding = pixels;
476         requestLayout();
477     }
478 
479     /**
480      * Get the horizontal padding around the text drawn on the switch itself.
481      *
482      * @return Horizontal padding for switch thumb text in pixels
483      *
484      * @attr ref android.R.styleable#Switch_thumbTextPadding
485      */
getThumbTextPadding()486     public int getThumbTextPadding() {
487         return mThumbTextPadding;
488     }
489 
490     /**
491      * Set the drawable used for the track that the switch slides within.
492      *
493      * @param track Track drawable
494      *
495      * @attr ref android.R.styleable#Switch_track
496      */
setTrackDrawable(Drawable track)497     public void setTrackDrawable(Drawable track) {
498         if (mTrackDrawable != null) {
499             mTrackDrawable.setCallback(null);
500         }
501         mTrackDrawable = track;
502         if (track != null) {
503             track.setCallback(this);
504         }
505         requestLayout();
506     }
507 
508     /**
509      * Set the drawable used for the track that the switch slides within.
510      *
511      * @param resId Resource ID of a track drawable
512      *
513      * @attr ref android.R.styleable#Switch_track
514      */
setTrackResource(@rawableRes int resId)515     public void setTrackResource(@DrawableRes int resId) {
516         setTrackDrawable(getContext().getDrawable(resId));
517     }
518 
519     /**
520      * Get the drawable used for the track that the switch slides within.
521      *
522      * @return Track drawable
523      *
524      * @attr ref android.R.styleable#Switch_track
525      */
getTrackDrawable()526     public Drawable getTrackDrawable() {
527         return mTrackDrawable;
528     }
529 
530     /**
531      * Applies a tint to the track drawable. Does not modify the current
532      * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
533      * <p>
534      * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
535      * automatically mutate the drawable and apply the specified tint and tint
536      * mode using {@link Drawable#setTintList(ColorStateList)}.
537      *
538      * @param tint the tint to apply, may be {@code null} to clear tint
539      *
540      * @attr ref android.R.styleable#Switch_trackTint
541      * @see #getTrackTintList()
542      * @see Drawable#setTintList(ColorStateList)
543      */
setTrackTintList(@ullable ColorStateList tint)544     public void setTrackTintList(@Nullable ColorStateList tint) {
545         mTrackTintList = tint;
546         mHasTrackTint = true;
547 
548         applyTrackTint();
549     }
550 
551     /**
552      * @return the tint applied to the track drawable
553      * @attr ref android.R.styleable#Switch_trackTint
554      * @see #setTrackTintList(ColorStateList)
555      */
556     @Nullable
getTrackTintList()557     public ColorStateList getTrackTintList() {
558         return mTrackTintList;
559     }
560 
561     /**
562      * Specifies the blending mode used to apply the tint specified by
563      * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
564      * The default mode is {@link PorterDuff.Mode#SRC_IN}.
565      *
566      * @param tintMode the blending mode used to apply the tint, may be
567      *                 {@code null} to clear tint
568      * @attr ref android.R.styleable#Switch_trackTintMode
569      * @see #getTrackTintMode()
570      * @see Drawable#setTintMode(PorterDuff.Mode)
571      */
setTrackTintMode(@ullable PorterDuff.Mode tintMode)572     public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
573         mTrackTintMode = tintMode;
574         mHasTrackTintMode = true;
575 
576         applyTrackTint();
577     }
578 
579     /**
580      * @return the blending mode used to apply the tint to the track
581      *         drawable
582      * @attr ref android.R.styleable#Switch_trackTintMode
583      * @see #setTrackTintMode(PorterDuff.Mode)
584      */
585     @Nullable
getTrackTintMode()586     public PorterDuff.Mode getTrackTintMode() {
587         return mTrackTintMode;
588     }
589 
applyTrackTint()590     private void applyTrackTint() {
591         if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
592             mTrackDrawable = mTrackDrawable.mutate();
593 
594             if (mHasTrackTint) {
595                 mTrackDrawable.setTintList(mTrackTintList);
596             }
597 
598             if (mHasTrackTintMode) {
599                 mTrackDrawable.setTintMode(mTrackTintMode);
600             }
601 
602             // The drawable (or one of its children) may not have been
603             // stateful before applying the tint, so let's try again.
604             if (mTrackDrawable.isStateful()) {
605                 mTrackDrawable.setState(getDrawableState());
606             }
607         }
608     }
609 
610     /**
611      * Set the drawable used for the switch "thumb" - the piece that the user
612      * can physically touch and drag along the track.
613      *
614      * @param thumb Thumb drawable
615      *
616      * @attr ref android.R.styleable#Switch_thumb
617      */
setThumbDrawable(Drawable thumb)618     public void setThumbDrawable(Drawable thumb) {
619         if (mThumbDrawable != null) {
620             mThumbDrawable.setCallback(null);
621         }
622         mThumbDrawable = thumb;
623         if (thumb != null) {
624             thumb.setCallback(this);
625         }
626         requestLayout();
627     }
628 
629     /**
630      * Set the drawable used for the switch "thumb" - the piece that the user
631      * can physically touch and drag along the track.
632      *
633      * @param resId Resource ID of a thumb drawable
634      *
635      * @attr ref android.R.styleable#Switch_thumb
636      */
setThumbResource(@rawableRes int resId)637     public void setThumbResource(@DrawableRes int resId) {
638         setThumbDrawable(getContext().getDrawable(resId));
639     }
640 
641     /**
642      * Get the drawable used for the switch "thumb" - the piece that the user
643      * can physically touch and drag along the track.
644      *
645      * @return Thumb drawable
646      *
647      * @attr ref android.R.styleable#Switch_thumb
648      */
getThumbDrawable()649     public Drawable getThumbDrawable() {
650         return mThumbDrawable;
651     }
652 
653     /**
654      * Applies a tint to the thumb drawable. Does not modify the current
655      * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
656      * <p>
657      * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
658      * automatically mutate the drawable and apply the specified tint and tint
659      * mode using {@link Drawable#setTintList(ColorStateList)}.
660      *
661      * @param tint the tint to apply, may be {@code null} to clear tint
662      *
663      * @attr ref android.R.styleable#Switch_thumbTint
664      * @see #getThumbTintList()
665      * @see Drawable#setTintList(ColorStateList)
666      */
setThumbTintList(@ullable ColorStateList tint)667     public void setThumbTintList(@Nullable ColorStateList tint) {
668         mThumbTintList = tint;
669         mHasThumbTint = true;
670 
671         applyThumbTint();
672     }
673 
674     /**
675      * @return the tint applied to the thumb drawable
676      * @attr ref android.R.styleable#Switch_thumbTint
677      * @see #setThumbTintList(ColorStateList)
678      */
679     @Nullable
getThumbTintList()680     public ColorStateList getThumbTintList() {
681         return mThumbTintList;
682     }
683 
684     /**
685      * Specifies the blending mode used to apply the tint specified by
686      * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
687      * The default mode is {@link PorterDuff.Mode#SRC_IN}.
688      *
689      * @param tintMode the blending mode used to apply the tint, may be
690      *                 {@code null} to clear tint
691      * @attr ref android.R.styleable#Switch_thumbTintMode
692      * @see #getThumbTintMode()
693      * @see Drawable#setTintMode(PorterDuff.Mode)
694      */
setThumbTintMode(@ullable PorterDuff.Mode tintMode)695     public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
696         mThumbTintMode = tintMode;
697         mHasThumbTintMode = true;
698 
699         applyThumbTint();
700     }
701 
702     /**
703      * @return the blending mode used to apply the tint to the thumb
704      *         drawable
705      * @attr ref android.R.styleable#Switch_thumbTintMode
706      * @see #setThumbTintMode(PorterDuff.Mode)
707      */
708     @Nullable
getThumbTintMode()709     public PorterDuff.Mode getThumbTintMode() {
710         return mThumbTintMode;
711     }
712 
applyThumbTint()713     private void applyThumbTint() {
714         if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
715             mThumbDrawable = mThumbDrawable.mutate();
716 
717             if (mHasThumbTint) {
718                 mThumbDrawable.setTintList(mThumbTintList);
719             }
720 
721             if (mHasThumbTintMode) {
722                 mThumbDrawable.setTintMode(mThumbTintMode);
723             }
724 
725             // The drawable (or one of its children) may not have been
726             // stateful before applying the tint, so let's try again.
727             if (mThumbDrawable.isStateful()) {
728                 mThumbDrawable.setState(getDrawableState());
729             }
730         }
731     }
732 
733     /**
734      * Specifies whether the track should be split by the thumb. When true,
735      * the thumb's optical bounds will be clipped out of the track drawable,
736      * then the thumb will be drawn into the resulting gap.
737      *
738      * @param splitTrack Whether the track should be split by the thumb
739      *
740      * @attr ref android.R.styleable#Switch_splitTrack
741      */
setSplitTrack(boolean splitTrack)742     public void setSplitTrack(boolean splitTrack) {
743         mSplitTrack = splitTrack;
744         invalidate();
745     }
746 
747     /**
748      * Returns whether the track should be split by the thumb.
749      *
750      * @attr ref android.R.styleable#Switch_splitTrack
751      */
getSplitTrack()752     public boolean getSplitTrack() {
753         return mSplitTrack;
754     }
755 
756     /**
757      * Returns the text displayed when the button is in the checked state.
758      *
759      * @attr ref android.R.styleable#Switch_textOn
760      */
getTextOn()761     public CharSequence getTextOn() {
762         return mTextOn;
763     }
764 
765     /**
766      * Sets the text displayed when the button is in the checked state.
767      *
768      * @attr ref android.R.styleable#Switch_textOn
769      */
setTextOn(CharSequence textOn)770     public void setTextOn(CharSequence textOn) {
771         mTextOn = textOn;
772         requestLayout();
773     }
774 
775     /**
776      * Returns the text displayed when the button is not in the checked state.
777      *
778      * @attr ref android.R.styleable#Switch_textOff
779      */
getTextOff()780     public CharSequence getTextOff() {
781         return mTextOff;
782     }
783 
784     /**
785      * Sets the text displayed when the button is not in the checked state.
786      *
787      * @attr ref android.R.styleable#Switch_textOff
788      */
setTextOff(CharSequence textOff)789     public void setTextOff(CharSequence textOff) {
790         mTextOff = textOff;
791         requestLayout();
792     }
793 
794     /**
795      * Sets whether the on/off text should be displayed.
796      *
797      * @param showText {@code true} to display on/off text
798      * @attr ref android.R.styleable#Switch_showText
799      */
setShowText(boolean showText)800     public void setShowText(boolean showText) {
801         if (mShowText != showText) {
802             mShowText = showText;
803             requestLayout();
804         }
805     }
806 
807     /**
808      * @return whether the on/off text should be displayed
809      * @attr ref android.R.styleable#Switch_showText
810      */
getShowText()811     public boolean getShowText() {
812         return mShowText;
813     }
814 
815     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)816     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
817         if (mShowText) {
818             if (mOnLayout == null) {
819                 mOnLayout = makeLayout(mTextOn);
820             }
821 
822             if (mOffLayout == null) {
823                 mOffLayout = makeLayout(mTextOff);
824             }
825         }
826 
827         final Rect padding = mTempRect;
828         final int thumbWidth;
829         final int thumbHeight;
830         if (mThumbDrawable != null) {
831             // Cached thumb width does not include padding.
832             mThumbDrawable.getPadding(padding);
833             thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
834             thumbHeight = mThumbDrawable.getIntrinsicHeight();
835         } else {
836             thumbWidth = 0;
837             thumbHeight = 0;
838         }
839 
840         final int maxTextWidth;
841         if (mShowText) {
842             maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
843                     + mThumbTextPadding * 2;
844         } else {
845             maxTextWidth = 0;
846         }
847 
848         mThumbWidth = Math.max(maxTextWidth, thumbWidth);
849 
850         final int trackHeight;
851         if (mTrackDrawable != null) {
852             mTrackDrawable.getPadding(padding);
853             trackHeight = mTrackDrawable.getIntrinsicHeight();
854         } else {
855             padding.setEmpty();
856             trackHeight = 0;
857         }
858 
859         // Adjust left and right padding to ensure there's enough room for the
860         // thumb's padding (when present).
861         int paddingLeft = padding.left;
862         int paddingRight = padding.right;
863         if (mThumbDrawable != null) {
864             final Insets inset = mThumbDrawable.getOpticalInsets();
865             paddingLeft = Math.max(paddingLeft, inset.left);
866             paddingRight = Math.max(paddingRight, inset.right);
867         }
868 
869         final int switchWidth = Math.max(mSwitchMinWidth,
870                 2 * mThumbWidth + paddingLeft + paddingRight);
871         final int switchHeight = Math.max(trackHeight, thumbHeight);
872         mSwitchWidth = switchWidth;
873         mSwitchHeight = switchHeight;
874 
875         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
876 
877         final int measuredHeight = getMeasuredHeight();
878         if (measuredHeight < switchHeight) {
879             setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
880         }
881     }
882 
883     /** @hide */
884     @Override
onPopulateAccessibilityEventInternal(AccessibilityEvent event)885     public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
886         super.onPopulateAccessibilityEventInternal(event);
887 
888         final CharSequence text = isChecked() ? mTextOn : mTextOff;
889         if (text != null) {
890             event.getText().add(text);
891         }
892     }
893 
makeLayout(CharSequence text)894     private Layout makeLayout(CharSequence text) {
895         final CharSequence transformed = (mSwitchTransformationMethod != null)
896                     ? mSwitchTransformationMethod.getTransformation(text, this)
897                     : text;
898 
899         int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
900                 transformed.length(), mTextPaint, getTextDirectionHeuristic()));
901         return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width)
902                 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
903                 .build();
904     }
905 
906     /**
907      * @return true if (x, y) is within the target area of the switch thumb
908      */
hitThumb(float x, float y)909     private boolean hitThumb(float x, float y) {
910         if (mThumbDrawable == null) {
911             return false;
912         }
913 
914         // Relies on mTempRect, MUST be called first!
915         final int thumbOffset = getThumbOffset();
916 
917         mThumbDrawable.getPadding(mTempRect);
918         final int thumbTop = mSwitchTop - mTouchSlop;
919         final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
920         final int thumbRight = thumbLeft + mThumbWidth +
921                 mTempRect.left + mTempRect.right + mTouchSlop;
922         final int thumbBottom = mSwitchBottom + mTouchSlop;
923         return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
924     }
925 
926     @Override
onTouchEvent(MotionEvent ev)927     public boolean onTouchEvent(MotionEvent ev) {
928         mVelocityTracker.addMovement(ev);
929         final int action = ev.getActionMasked();
930         switch (action) {
931             case MotionEvent.ACTION_DOWN: {
932                 final float x = ev.getX();
933                 final float y = ev.getY();
934                 if (isEnabled() && hitThumb(x, y)) {
935                     mTouchMode = TOUCH_MODE_DOWN;
936                     mTouchX = x;
937                     mTouchY = y;
938                 }
939                 break;
940             }
941 
942             case MotionEvent.ACTION_MOVE: {
943                 switch (mTouchMode) {
944                     case TOUCH_MODE_IDLE:
945                         // Didn't target the thumb, treat normally.
946                         break;
947 
948                     case TOUCH_MODE_DOWN: {
949                         final float x = ev.getX();
950                         final float y = ev.getY();
951                         if (Math.abs(x - mTouchX) > mTouchSlop ||
952                                 Math.abs(y - mTouchY) > mTouchSlop) {
953                             mTouchMode = TOUCH_MODE_DRAGGING;
954                             getParent().requestDisallowInterceptTouchEvent(true);
955                             mTouchX = x;
956                             mTouchY = y;
957                             return true;
958                         }
959                         break;
960                     }
961 
962                     case TOUCH_MODE_DRAGGING: {
963                         final float x = ev.getX();
964                         final int thumbScrollRange = getThumbScrollRange();
965                         final float thumbScrollOffset = x - mTouchX;
966                         float dPos;
967                         if (thumbScrollRange != 0) {
968                             dPos = thumbScrollOffset / thumbScrollRange;
969                         } else {
970                             // If the thumb scroll range is empty, just use the
971                             // movement direction to snap on or off.
972                             dPos = thumbScrollOffset > 0 ? 1 : -1;
973                         }
974                         if (isLayoutRtl()) {
975                             dPos = -dPos;
976                         }
977                         final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
978                         if (newPos != mThumbPosition) {
979                             mTouchX = x;
980                             setThumbPosition(newPos);
981                         }
982                         return true;
983                     }
984                 }
985                 break;
986             }
987 
988             case MotionEvent.ACTION_UP:
989             case MotionEvent.ACTION_CANCEL: {
990                 if (mTouchMode == TOUCH_MODE_DRAGGING) {
991                     stopDrag(ev);
992                     // Allow super class to handle pressed state, etc.
993                     super.onTouchEvent(ev);
994                     return true;
995                 }
996                 mTouchMode = TOUCH_MODE_IDLE;
997                 mVelocityTracker.clear();
998                 break;
999             }
1000         }
1001 
1002         return super.onTouchEvent(ev);
1003     }
1004 
cancelSuperTouch(MotionEvent ev)1005     private void cancelSuperTouch(MotionEvent ev) {
1006         MotionEvent cancel = MotionEvent.obtain(ev);
1007         cancel.setAction(MotionEvent.ACTION_CANCEL);
1008         super.onTouchEvent(cancel);
1009         cancel.recycle();
1010     }
1011 
1012     /**
1013      * Called from onTouchEvent to end a drag operation.
1014      *
1015      * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1016      */
stopDrag(MotionEvent ev)1017     private void stopDrag(MotionEvent ev) {
1018         mTouchMode = TOUCH_MODE_IDLE;
1019 
1020         // Commit the change if the event is up and not canceled and the switch
1021         // has not been disabled during the drag.
1022         final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
1023         final boolean oldState = isChecked();
1024         final boolean newState;
1025         if (commitChange) {
1026             mVelocityTracker.computeCurrentVelocity(1000);
1027             final float xvel = mVelocityTracker.getXVelocity();
1028             if (Math.abs(xvel) > mMinFlingVelocity) {
1029                 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
1030             } else {
1031                 newState = getTargetCheckedState();
1032             }
1033         } else {
1034             newState = oldState;
1035         }
1036 
1037         if (newState != oldState) {
1038             playSoundEffect(SoundEffectConstants.CLICK);
1039         }
1040         // Always call setChecked so that the thumb is moved back to the correct edge
1041         setChecked(newState);
1042         cancelSuperTouch(ev);
1043     }
1044 
animateThumbToCheckedState(boolean newCheckedState)1045     private void animateThumbToCheckedState(boolean newCheckedState) {
1046         final float targetPosition = newCheckedState ? 1 : 0;
1047         mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1048         mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1049         mPositionAnimator.setAutoCancel(true);
1050         mPositionAnimator.start();
1051     }
1052 
cancelPositionAnimator()1053     private void cancelPositionAnimator() {
1054         if (mPositionAnimator != null) {
1055             mPositionAnimator.cancel();
1056         }
1057     }
1058 
getTargetCheckedState()1059     private boolean getTargetCheckedState() {
1060         return mThumbPosition > 0.5f;
1061     }
1062 
1063     /**
1064      * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1065      *
1066      * @param position new position between [0,1]
1067      */
setThumbPosition(float position)1068     private void setThumbPosition(float position) {
1069         mThumbPosition = position;
1070         invalidate();
1071     }
1072 
1073     @Override
toggle()1074     public void toggle() {
1075         setChecked(!isChecked());
1076     }
1077 
1078     @Override
setChecked(boolean checked)1079     public void setChecked(boolean checked) {
1080         super.setChecked(checked);
1081 
1082         // Calling the super method may result in setChecked() getting called
1083         // recursively with a different value, so load the REAL value...
1084         checked = isChecked();
1085 
1086         if (isAttachedToWindow() && isLaidOut()) {
1087             animateThumbToCheckedState(checked);
1088         } else {
1089             // Immediately move the thumb to the new position.
1090             cancelPositionAnimator();
1091             setThumbPosition(checked ? 1 : 0);
1092         }
1093     }
1094 
1095     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)1096     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1097         super.onLayout(changed, left, top, right, bottom);
1098 
1099         int opticalInsetLeft = 0;
1100         int opticalInsetRight = 0;
1101         if (mThumbDrawable != null) {
1102             final Rect trackPadding = mTempRect;
1103             if (mTrackDrawable != null) {
1104                 mTrackDrawable.getPadding(trackPadding);
1105             } else {
1106                 trackPadding.setEmpty();
1107             }
1108 
1109             final Insets insets = mThumbDrawable.getOpticalInsets();
1110             opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1111             opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
1112         }
1113 
1114         final int switchRight;
1115         final int switchLeft;
1116         if (isLayoutRtl()) {
1117             switchLeft = getPaddingLeft() + opticalInsetLeft;
1118             switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1119         } else {
1120             switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1121             switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1122         }
1123 
1124         final int switchTop;
1125         final int switchBottom;
1126         switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1127             default:
1128             case Gravity.TOP:
1129                 switchTop = getPaddingTop();
1130                 switchBottom = switchTop + mSwitchHeight;
1131                 break;
1132 
1133             case Gravity.CENTER_VERTICAL:
1134                 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1135                         mSwitchHeight / 2;
1136                 switchBottom = switchTop + mSwitchHeight;
1137                 break;
1138 
1139             case Gravity.BOTTOM:
1140                 switchBottom = getHeight() - getPaddingBottom();
1141                 switchTop = switchBottom - mSwitchHeight;
1142                 break;
1143         }
1144 
1145         mSwitchLeft = switchLeft;
1146         mSwitchTop = switchTop;
1147         mSwitchBottom = switchBottom;
1148         mSwitchRight = switchRight;
1149     }
1150 
1151     @Override
draw(Canvas c)1152     public void draw(Canvas c) {
1153         final Rect padding = mTempRect;
1154         final int switchLeft = mSwitchLeft;
1155         final int switchTop = mSwitchTop;
1156         final int switchRight = mSwitchRight;
1157         final int switchBottom = mSwitchBottom;
1158 
1159         int thumbInitialLeft = switchLeft + getThumbOffset();
1160 
1161         final Insets thumbInsets;
1162         if (mThumbDrawable != null) {
1163             thumbInsets = mThumbDrawable.getOpticalInsets();
1164         } else {
1165             thumbInsets = Insets.NONE;
1166         }
1167 
1168         // Layout the track.
1169         if (mTrackDrawable != null) {
1170             mTrackDrawable.getPadding(padding);
1171 
1172             // Adjust thumb position for track padding.
1173             thumbInitialLeft += padding.left;
1174 
1175             // If necessary, offset by the optical insets of the thumb asset.
1176             int trackLeft = switchLeft;
1177             int trackTop = switchTop;
1178             int trackRight = switchRight;
1179             int trackBottom = switchBottom;
1180             if (thumbInsets != Insets.NONE) {
1181                 if (thumbInsets.left > padding.left) {
1182                     trackLeft += thumbInsets.left - padding.left;
1183                 }
1184                 if (thumbInsets.top > padding.top) {
1185                     trackTop += thumbInsets.top - padding.top;
1186                 }
1187                 if (thumbInsets.right > padding.right) {
1188                     trackRight -= thumbInsets.right - padding.right;
1189                 }
1190                 if (thumbInsets.bottom > padding.bottom) {
1191                     trackBottom -= thumbInsets.bottom - padding.bottom;
1192                 }
1193             }
1194             mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1195         }
1196 
1197         // Layout the thumb.
1198         if (mThumbDrawable != null) {
1199             mThumbDrawable.getPadding(padding);
1200 
1201             final int thumbLeft = thumbInitialLeft - padding.left;
1202             final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
1203             mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1204 
1205             final Drawable background = getBackground();
1206             if (background != null) {
1207                 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1208             }
1209         }
1210 
1211         // Draw the background.
1212         super.draw(c);
1213     }
1214 
1215     @Override
onDraw(Canvas canvas)1216     protected void onDraw(Canvas canvas) {
1217         super.onDraw(canvas);
1218 
1219         final Rect padding = mTempRect;
1220         final Drawable trackDrawable = mTrackDrawable;
1221         if (trackDrawable != null) {
1222             trackDrawable.getPadding(padding);
1223         } else {
1224             padding.setEmpty();
1225         }
1226 
1227         final int switchTop = mSwitchTop;
1228         final int switchBottom = mSwitchBottom;
1229         final int switchInnerTop = switchTop + padding.top;
1230         final int switchInnerBottom = switchBottom - padding.bottom;
1231 
1232         final Drawable thumbDrawable = mThumbDrawable;
1233         if (trackDrawable != null) {
1234             if (mSplitTrack && thumbDrawable != null) {
1235                 final Insets insets = thumbDrawable.getOpticalInsets();
1236                 thumbDrawable.copyBounds(padding);
1237                 padding.left += insets.left;
1238                 padding.right -= insets.right;
1239 
1240                 final int saveCount = canvas.save();
1241                 canvas.clipRect(padding, Op.DIFFERENCE);
1242                 trackDrawable.draw(canvas);
1243                 canvas.restoreToCount(saveCount);
1244             } else {
1245                 trackDrawable.draw(canvas);
1246             }
1247         }
1248 
1249         final int saveCount = canvas.save();
1250 
1251         if (thumbDrawable != null) {
1252             thumbDrawable.draw(canvas);
1253         }
1254 
1255         final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
1256         if (switchText != null) {
1257             final int drawableState[] = getDrawableState();
1258             if (mTextColors != null) {
1259                 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1260             }
1261             mTextPaint.drawableState = drawableState;
1262 
1263             final int cX;
1264             if (thumbDrawable != null) {
1265                 final Rect bounds = thumbDrawable.getBounds();
1266                 cX = bounds.left + bounds.right;
1267             } else {
1268                 cX = getWidth();
1269             }
1270 
1271             final int left = cX / 2 - switchText.getWidth() / 2;
1272             final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1273             canvas.translate(left, top);
1274             switchText.draw(canvas);
1275         }
1276 
1277         canvas.restoreToCount(saveCount);
1278     }
1279 
1280     @Override
getCompoundPaddingLeft()1281     public int getCompoundPaddingLeft() {
1282         if (!isLayoutRtl()) {
1283             return super.getCompoundPaddingLeft();
1284         }
1285         int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1286         if (!TextUtils.isEmpty(getText())) {
1287             padding += mSwitchPadding;
1288         }
1289         return padding;
1290     }
1291 
1292     @Override
getCompoundPaddingRight()1293     public int getCompoundPaddingRight() {
1294         if (isLayoutRtl()) {
1295             return super.getCompoundPaddingRight();
1296         }
1297         int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1298         if (!TextUtils.isEmpty(getText())) {
1299             padding += mSwitchPadding;
1300         }
1301         return padding;
1302     }
1303 
1304     /**
1305      * Translates thumb position to offset according to current RTL setting and
1306      * thumb scroll range. Accounts for both track and thumb padding.
1307      *
1308      * @return thumb offset
1309      */
getThumbOffset()1310     private int getThumbOffset() {
1311         final float thumbPosition;
1312         if (isLayoutRtl()) {
1313             thumbPosition = 1 - mThumbPosition;
1314         } else {
1315             thumbPosition = mThumbPosition;
1316         }
1317         return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1318     }
1319 
getThumbScrollRange()1320     private int getThumbScrollRange() {
1321         if (mTrackDrawable != null) {
1322             final Rect padding = mTempRect;
1323             mTrackDrawable.getPadding(padding);
1324 
1325             final Insets insets;
1326             if (mThumbDrawable != null) {
1327                 insets = mThumbDrawable.getOpticalInsets();
1328             } else {
1329                 insets = Insets.NONE;
1330             }
1331 
1332             return mSwitchWidth - mThumbWidth - padding.left - padding.right
1333                     - insets.left - insets.right;
1334         } else {
1335             return 0;
1336         }
1337     }
1338 
1339     @Override
onCreateDrawableState(int extraSpace)1340     protected int[] onCreateDrawableState(int extraSpace) {
1341         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1342         if (isChecked()) {
1343             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1344         }
1345         return drawableState;
1346     }
1347 
1348     @Override
drawableStateChanged()1349     protected void drawableStateChanged() {
1350         super.drawableStateChanged();
1351 
1352         final int[] state = getDrawableState();
1353         boolean changed = false;
1354 
1355         final Drawable thumbDrawable = mThumbDrawable;
1356         if (thumbDrawable != null && thumbDrawable.isStateful()) {
1357             changed |= thumbDrawable.setState(state);
1358         }
1359 
1360         final Drawable trackDrawable = mTrackDrawable;
1361         if (trackDrawable != null && trackDrawable.isStateful()) {
1362             changed |= trackDrawable.setState(state);
1363         }
1364 
1365         if (changed) {
1366             invalidate();
1367         }
1368     }
1369 
1370     @Override
drawableHotspotChanged(float x, float y)1371     public void drawableHotspotChanged(float x, float y) {
1372         super.drawableHotspotChanged(x, y);
1373 
1374         if (mThumbDrawable != null) {
1375             mThumbDrawable.setHotspot(x, y);
1376         }
1377 
1378         if (mTrackDrawable != null) {
1379             mTrackDrawable.setHotspot(x, y);
1380         }
1381     }
1382 
1383     @Override
verifyDrawable(@onNull Drawable who)1384     protected boolean verifyDrawable(@NonNull Drawable who) {
1385         return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1386     }
1387 
1388     @Override
jumpDrawablesToCurrentState()1389     public void jumpDrawablesToCurrentState() {
1390         super.jumpDrawablesToCurrentState();
1391 
1392         if (mThumbDrawable != null) {
1393             mThumbDrawable.jumpToCurrentState();
1394         }
1395 
1396         if (mTrackDrawable != null) {
1397             mTrackDrawable.jumpToCurrentState();
1398         }
1399 
1400         if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
1401             mPositionAnimator.end();
1402             mPositionAnimator = null;
1403         }
1404     }
1405 
1406     @Override
getAccessibilityClassName()1407     public CharSequence getAccessibilityClassName() {
1408         return Switch.class.getName();
1409     }
1410 
1411     @Override
onProvideStructure(ViewStructure structure)1412     public void onProvideStructure(ViewStructure structure) {
1413         super.onProvideStructure(structure);
1414         onProvideAutoFillStructureForAssistOrAutofill(structure);
1415     }
1416 
1417     @Override
onProvideAutofillStructure(ViewStructure structure, int flags)1418     public void onProvideAutofillStructure(ViewStructure structure, int flags) {
1419         super.onProvideAutofillStructure(structure, flags);
1420         onProvideAutoFillStructureForAssistOrAutofill(structure);
1421     }
1422 
1423     // NOTE: currently there is no difference for Assist or AutoFill, so it doesn't take flags
onProvideAutoFillStructureForAssistOrAutofill(ViewStructure structure)1424     private void onProvideAutoFillStructureForAssistOrAutofill(ViewStructure structure) {
1425         CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1426         if (!TextUtils.isEmpty(switchText)) {
1427             CharSequence oldText = structure.getText();
1428             if (TextUtils.isEmpty(oldText)) {
1429                 structure.setText(switchText);
1430             } else {
1431                 StringBuilder newText = new StringBuilder();
1432                 newText.append(oldText).append(' ').append(switchText);
1433                 structure.setText(newText);
1434             }
1435             // The style of the label text is provided via the base TextView class. This is more
1436             // relevant than the style of the (optional) on/off text on the switch button itself,
1437             // so ignore the size/color/style stored this.mTextPaint.
1438         }
1439     }
1440 
1441     /** @hide */
1442     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1443     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1444         super.onInitializeAccessibilityNodeInfoInternal(info);
1445         CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1446         if (!TextUtils.isEmpty(switchText)) {
1447             CharSequence oldText = info.getText();
1448             if (TextUtils.isEmpty(oldText)) {
1449                 info.setText(switchText);
1450             } else {
1451                 StringBuilder newText = new StringBuilder();
1452                 newText.append(oldText).append(' ').append(switchText);
1453                 info.setText(newText);
1454             }
1455         }
1456     }
1457 
1458     private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1459         @Override
1460         public Float get(Switch object) {
1461             return object.mThumbPosition;
1462         }
1463 
1464         @Override
1465         public void setValue(Switch object, float value) {
1466             object.setThumbPosition(value);
1467         }
1468     };
1469 }
1470