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