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