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