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