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