SwitchCompat.java revision 36ba98b397a95e5d74318f7d9008fdaf604c05c2
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import android.animation.ObjectAnimator;
20import android.annotation.TargetApi;
21import android.content.Context;
22import android.content.res.ColorStateList;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
30import android.os.Build;
31import android.support.v4.graphics.drawable.DrawableCompat;
32import android.support.v4.view.MotionEventCompat;
33import android.support.v4.view.ViewCompat;
34import android.support.v7.appcompat.R;
35import android.support.v7.internal.text.AllCapsTransformationMethod;
36import android.support.v7.internal.widget.TintManager;
37import android.support.v7.internal.widget.TintTypedArray;
38import android.support.v7.internal.widget.ViewUtils;
39import android.text.Layout;
40import android.text.StaticLayout;
41import android.text.TextPaint;
42import android.text.TextUtils;
43import android.text.method.TransformationMethod;
44import android.util.AttributeSet;
45import android.view.Gravity;
46import android.view.MotionEvent;
47import android.view.SoundEffectConstants;
48import android.view.VelocityTracker;
49import android.view.ViewConfiguration;
50import android.view.accessibility.AccessibilityEvent;
51import android.view.accessibility.AccessibilityNodeInfo;
52import android.view.animation.Animation;
53import android.view.animation.Transformation;
54import android.widget.CompoundButton;
55
56/**
57 * SwitchCompat is a version of the Switch widget which on devices back to API v7. It does not
58 * make any attempt to use the platform provided widget on those devices which it is available
59 * normally.
60 * <p>
61 * A Switch is a two-state toggle switch widget that can select between two
62 * options. The user may drag the "thumb" back and forth to choose the selected option,
63 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
64 * property controls the text displayed in the label for the switch, whereas the
65 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
66 * controls the text on the thumb. Similarly, the
67 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
68 * setTypeface() methods control the typeface and style of label text, whereas the
69 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
70 * the related seSwitchTypeface() methods control that of the thumb.
71 */
72public class SwitchCompat extends CompoundButton {
73    private static final int THUMB_ANIMATION_DURATION = 250;
74
75    private static final int TOUCH_MODE_IDLE = 0;
76    private static final int TOUCH_MODE_DOWN = 1;
77    private static final int TOUCH_MODE_DRAGGING = 2;
78
79    // Enum for the "typeface" XML parameter.
80    private static final int SANS = 1;
81    private static final int SERIF = 2;
82    private static final int MONOSPACE = 3;
83
84    private Drawable mThumbDrawable;
85    private Drawable mTrackDrawable;
86    private int mThumbTextPadding;
87    private int mSwitchMinWidth;
88    private int mSwitchPadding;
89    private boolean mSplitTrack;
90    private CharSequence mTextOn;
91    private CharSequence mTextOff;
92    private boolean mShowText;
93
94    private int mTouchMode;
95    private int mTouchSlop;
96    private float mTouchX;
97    private float mTouchY;
98    private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
99    private int mMinFlingVelocity;
100
101    private float mThumbPosition;
102
103    /**
104     * Width required to draw the switch track and thumb. Includes padding and
105     * optical bounds for both the track and thumb.
106     */
107    private int mSwitchWidth;
108
109    /**
110     * Height required to draw the switch track and thumb. Includes padding and
111     * optical bounds for both the track and thumb.
112     */
113    private int mSwitchHeight;
114
115    /**
116     * Width of the thumb's content region. Does not include padding or
117     * optical bounds.
118     */
119    private int mThumbWidth;
120
121    /** Left bound for drawing the switch track and thumb. */
122    private int mSwitchLeft;
123
124    /** Top bound for drawing the switch track and thumb. */
125    private int mSwitchTop;
126
127    /** Right bound for drawing the switch track and thumb. */
128    private int mSwitchRight;
129
130    /** Bottom bound for drawing the switch track and thumb. */
131    private int mSwitchBottom;
132
133    private TextPaint mTextPaint;
134    private ColorStateList mTextColors;
135    private Layout mOnLayout;
136    private Layout mOffLayout;
137    private TransformationMethod mSwitchTransformationMethod;
138    private Animation mPositionAnimator;
139
140    @SuppressWarnings("hiding")
141    private final Rect mTempRect = new Rect();
142
143    private final TintManager mTintManager;
144
145    private static final int[] CHECKED_STATE_SET = {
146            android.R.attr.state_checked
147    };
148
149    /**
150     * Construct a new Switch with default styling.
151     *
152     * @param context The Context that will determine this widget's theming.
153     */
154    public SwitchCompat(Context context) {
155        this(context, null);
156    }
157
158    /**
159     * Construct a new Switch with default styling, overriding specific style
160     * attributes as requested.
161     *
162     * @param context The Context that will determine this widget's theming.
163     * @param attrs Specification of attributes that should deviate from default styling.
164     */
165    public SwitchCompat(Context context, AttributeSet attrs) {
166        this(context, attrs, R.attr.switchStyle);
167    }
168
169    /**
170     * Construct a new Switch with a default style determined by the given theme attribute,
171     * overriding specific style attributes as requested.
172     *
173     * @param context The Context that will determine this widget's theming.
174     * @param attrs Specification of attributes that should deviate from the default styling.
175     * @param defStyleAttr An attribute in the current theme that contains a
176     *        reference to a style resource that supplies default values for
177     *        the view. Can be 0 to not look for defaults.
178     */
179    public SwitchCompat(Context context, AttributeSet attrs, int defStyleAttr) {
180        super(context, attrs, defStyleAttr);
181
182        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
183
184        final Resources res = getResources();
185        mTextPaint.density = res.getDisplayMetrics().density;
186
187        final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context,
188                attrs, R.styleable.SwitchCompat, defStyleAttr, 0);
189        mThumbDrawable = a.getDrawable(R.styleable.SwitchCompat_android_thumb);
190        mTrackDrawable = a.getDrawable(R.styleable.SwitchCompat_track);
191        mTextOn = a.getText(R.styleable.SwitchCompat_android_textOn);
192        mTextOff = a.getText(R.styleable.SwitchCompat_android_textOff);
193        mShowText = a.getBoolean(R.styleable.SwitchCompat_showText, true);
194        mThumbTextPadding = a.getDimensionPixelSize(
195                R.styleable.SwitchCompat_thumbTextPadding, 0);
196        mSwitchMinWidth = a.getDimensionPixelSize(
197                R.styleable.SwitchCompat_switchMinWidth, 0);
198        mSwitchPadding = a.getDimensionPixelSize(
199                R.styleable.SwitchCompat_switchPadding, 0);
200        mSplitTrack = a.getBoolean(R.styleable.SwitchCompat_splitTrack, false);
201
202        final int appearance = a.getResourceId(
203                R.styleable.SwitchCompat_switchTextAppearance, 0);
204        if (appearance != 0) {
205            setSwitchTextAppearance(context, appearance);
206        }
207
208        mTintManager = a.getTintManager();
209
210        a.recycle();
211
212        final ViewConfiguration config = ViewConfiguration.get(context);
213        mTouchSlop = config.getScaledTouchSlop();
214        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
215
216        // Refresh display with current params
217        refreshDrawableState();
218        setChecked(isChecked());
219    }
220
221    /**
222     * Sets the switch text color, size, style, hint color, and highlight color
223     * from the specified TextAppearance resource.
224     */
225    public void setSwitchTextAppearance(Context context, int resid) {
226        TypedArray appearance = context.obtainStyledAttributes(resid,
227                R.styleable.SwitchCompatTextAppearance);
228
229        ColorStateList colors;
230        int ts;
231
232        colors = appearance.getColorStateList(
233                R.styleable.SwitchCompatTextAppearance_android_textColor);
234        if (colors != null) {
235            mTextColors = colors;
236        } else {
237            // If no color set in TextAppearance, default to the view's textColor
238            mTextColors = getTextColors();
239        }
240
241        ts = appearance.getDimensionPixelSize(
242                R.styleable.SwitchCompatTextAppearance_android_textSize, 0);
243        if (ts != 0) {
244            if (ts != mTextPaint.getTextSize()) {
245                mTextPaint.setTextSize(ts);
246                requestLayout();
247            }
248        }
249
250        boolean allCaps = appearance.getBoolean(
251                R.styleable.SwitchCompatTextAppearance_textAllCaps, false);
252        if (allCaps) {
253            mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
254        } else {
255            mSwitchTransformationMethod = null;
256        }
257
258        appearance.recycle();
259    }
260
261    /**
262     * Sets the typeface and style in which the text should be displayed on the
263     * switch, and turns on the fake bold and italic bits in the Paint if the
264     * Typeface that you provided does not have all the bits in the
265     * style that you specified.
266     */
267    public void setSwitchTypeface(Typeface tf, int style) {
268        if (style > 0) {
269            if (tf == null) {
270                tf = Typeface.defaultFromStyle(style);
271            } else {
272                tf = Typeface.create(tf, style);
273            }
274
275            setSwitchTypeface(tf);
276            // now compute what (if any) algorithmic styling is needed
277            int typefaceStyle = tf != null ? tf.getStyle() : 0;
278            int need = style & ~typefaceStyle;
279            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
280            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
281        } else {
282            mTextPaint.setFakeBoldText(false);
283            mTextPaint.setTextSkewX(0);
284            setSwitchTypeface(tf);
285        }
286    }
287
288    /**
289     * Sets the typeface in which the text should be displayed on the switch.
290     * Note that not all Typeface families actually have bold and italic
291     * variants, so you may need to use
292     * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
293     * that you actually want.
294     */
295    public void setSwitchTypeface(Typeface tf) {
296        if (mTextPaint.getTypeface() != tf) {
297            mTextPaint.setTypeface(tf);
298
299            requestLayout();
300            invalidate();
301        }
302    }
303
304    /**
305     * Set the amount of horizontal padding between the switch and the associated text.
306     *
307     * @param pixels Amount of padding in pixels
308     */
309    public void setSwitchPadding(int pixels) {
310        mSwitchPadding = pixels;
311        requestLayout();
312    }
313
314    /**
315     * Get the amount of horizontal padding between the switch and the associated text.
316     *
317     * @return Amount of padding in pixels
318     */
319    public int getSwitchPadding() {
320        return mSwitchPadding;
321    }
322
323    /**
324     * Set the minimum width of the switch in pixels. The switch's width will be the maximum
325     * of this value and its measured width as determined by the switch drawables and text used.
326     *
327     * @param pixels Minimum width of the switch in pixels
328     */
329    public void setSwitchMinWidth(int pixels) {
330        mSwitchMinWidth = pixels;
331        requestLayout();
332    }
333
334    /**
335     * Get the minimum width of the switch in pixels. The switch's width will be the maximum
336     * of this value and its measured width as determined by the switch drawables and text used.
337     *
338     * @return Minimum width of the switch in pixels
339     */
340    public int getSwitchMinWidth() {
341        return mSwitchMinWidth;
342    }
343
344    /**
345     * Set the horizontal padding around the text drawn on the switch itself.
346     *
347     * @param pixels Horizontal padding for switch thumb text in pixels
348     */
349    public void setThumbTextPadding(int pixels) {
350        mThumbTextPadding = pixels;
351        requestLayout();
352    }
353
354    /**
355     * Get the horizontal padding around the text drawn on the switch itself.
356     *
357     * @return Horizontal padding for switch thumb text in pixels
358     */
359    public int getThumbTextPadding() {
360        return mThumbTextPadding;
361    }
362
363    /**
364     * Set the drawable used for the track that the switch slides within.
365     *
366     * @param track Track drawable
367     */
368    public void setTrackDrawable(Drawable track) {
369        mTrackDrawable = track;
370        requestLayout();
371    }
372
373    /**
374     * Set the drawable used for the track that the switch slides within.
375     *
376     * @param resId Resource ID of a track drawable
377     */
378    public void setTrackResource(int resId) {
379        setTrackDrawable(mTintManager.getDrawable(resId));
380    }
381
382    /**
383     * Get the drawable used for the track that the switch slides within.
384     *
385     * @return Track drawable
386     */
387    public Drawable getTrackDrawable() {
388        return mTrackDrawable;
389    }
390
391    /**
392     * Set the drawable used for the switch "thumb" - the piece that the user
393     * can physically touch and drag along the track.
394     *
395     * @param thumb Thumb drawable
396     */
397    public void setThumbDrawable(Drawable thumb) {
398        mThumbDrawable = thumb;
399        requestLayout();
400    }
401
402    /**
403     * Set the drawable used for the switch "thumb" - the piece that the user
404     * can physically touch and drag along the track.
405     *
406     * @param resId Resource ID of a thumb drawable
407     */
408    public void setThumbResource(int resId) {
409        setThumbDrawable(mTintManager.getDrawable(resId));
410    }
411
412    /**
413     * Get the drawable used for the switch "thumb" - the piece that the user
414     * can physically touch and drag along the track.
415     *
416     * @return Thumb drawable
417     */
418    public Drawable getThumbDrawable() {
419        return mThumbDrawable;
420    }
421
422    /**
423     * Specifies whether the track should be split by the thumb. When true,
424     * the thumb's optical bounds will be clipped out of the track drawable,
425     * then the thumb will be drawn into the resulting gap.
426     *
427     * @param splitTrack Whether the track should be split by the thumb
428     */
429    public void setSplitTrack(boolean splitTrack) {
430        mSplitTrack = splitTrack;
431        invalidate();
432    }
433
434    /**
435     * Returns whether the track should be split by the thumb.
436     */
437    public boolean getSplitTrack() {
438        return mSplitTrack;
439    }
440
441    /**
442     * Returns the text displayed when the button is in the checked state.
443     */
444    public CharSequence getTextOn() {
445        return mTextOn;
446    }
447
448    /**
449     * Sets the text displayed when the button is in the checked state.
450     */
451    public void setTextOn(CharSequence textOn) {
452        mTextOn = textOn;
453        requestLayout();
454    }
455
456    /**
457     * Returns the text displayed when the button is not in the checked state.
458     */
459    public CharSequence getTextOff() {
460        return mTextOff;
461    }
462
463    /**
464     * Sets the text displayed when the button is not in the checked state.
465     */
466    public void setTextOff(CharSequence textOff) {
467        mTextOff = textOff;
468        requestLayout();
469    }
470
471    /**
472     * Sets whether the on/off text should be displayed.
473     *
474     * @param showText {@code true} to display on/off text
475     */
476    public void setShowText(boolean showText) {
477        if (mShowText != showText) {
478            mShowText = showText;
479            requestLayout();
480        }
481    }
482
483    /**
484     * @return whether the on/off text should be displayed
485     */
486    public boolean getShowText() {
487        return mShowText;
488    }
489
490    @Override
491    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
492        if (mShowText) {
493            if (mOnLayout == null) {
494                mOnLayout = makeLayout(mTextOn);
495            }
496
497            if (mOffLayout == null) {
498                mOffLayout = makeLayout(mTextOff);
499            }
500        }
501
502        final Rect padding = mTempRect;
503        final int thumbWidth;
504        final int thumbHeight;
505        if (mThumbDrawable != null) {
506            // Cached thumb width does not include padding.
507            mThumbDrawable.getPadding(padding);
508            thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
509            thumbHeight = mThumbDrawable.getIntrinsicHeight();
510        } else {
511            thumbWidth = 0;
512            thumbHeight = 0;
513        }
514
515        final int maxTextWidth;
516        if (mShowText) {
517            maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
518                    + mThumbTextPadding * 2;
519        } else {
520            maxTextWidth = 0;
521        }
522
523        mThumbWidth = Math.max(maxTextWidth, thumbWidth);
524
525        final int trackHeight;
526        if (mTrackDrawable != null) {
527            mTrackDrawable.getPadding(padding);
528            trackHeight = mTrackDrawable.getIntrinsicHeight();
529        } else {
530            padding.setEmpty();
531            trackHeight = 0;
532        }
533
534        // Adjust left and right padding to ensure there's enough room for the
535        // thumb's padding (when present).
536        int paddingLeft = padding.left;
537        int paddingRight = padding.right;
538
539        final int switchWidth = Math.max(mSwitchMinWidth,
540                2 * mThumbWidth + paddingLeft + paddingRight);
541        final int switchHeight = Math.max(trackHeight, thumbHeight);
542        mSwitchWidth = switchWidth;
543        mSwitchHeight = switchHeight;
544
545        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
546
547        final int measuredHeight = getMeasuredHeight();
548        if (measuredHeight < switchHeight) {
549            setMeasuredDimension(ViewCompat.getMeasuredWidthAndState(this), switchHeight);
550        }
551    }
552
553    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
554    @Override
555    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
556        super.onPopulateAccessibilityEvent(event);
557
558        final CharSequence text = isChecked() ? mTextOn : mTextOff;
559        if (text != null) {
560            event.getText().add(text);
561        }
562    }
563
564    private Layout makeLayout(CharSequence text) {
565        final CharSequence transformed = (mSwitchTransformationMethod != null)
566                ? mSwitchTransformationMethod.getTransformation(text, this)
567                : text;
568
569        return new StaticLayout(transformed, mTextPaint,
570                (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
571                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
572    }
573
574    /**
575     * @return true if (x, y) is within the target area of the switch thumb
576     */
577    private boolean hitThumb(float x, float y) {
578        // Relies on mTempRect, MUST be called first!
579        final int thumbOffset = getThumbOffset();
580
581        mThumbDrawable.getPadding(mTempRect);
582        final int thumbTop = mSwitchTop - mTouchSlop;
583        final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
584        final int thumbRight = thumbLeft + mThumbWidth +
585                mTempRect.left + mTempRect.right + mTouchSlop;
586        final int thumbBottom = mSwitchBottom + mTouchSlop;
587        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
588    }
589
590    @Override
591    public boolean onTouchEvent(MotionEvent ev) {
592        mVelocityTracker.addMovement(ev);
593        final int action = MotionEventCompat.getActionMasked(ev);
594        switch (action) {
595            case MotionEvent.ACTION_DOWN: {
596                final float x = ev.getX();
597                final float y = ev.getY();
598                if (isEnabled() && hitThumb(x, y)) {
599                    mTouchMode = TOUCH_MODE_DOWN;
600                    mTouchX = x;
601                    mTouchY = y;
602                }
603                break;
604            }
605
606            case MotionEvent.ACTION_MOVE: {
607                switch (mTouchMode) {
608                    case TOUCH_MODE_IDLE:
609                        // Didn't target the thumb, treat normally.
610                        break;
611
612                    case TOUCH_MODE_DOWN: {
613                        final float x = ev.getX();
614                        final float y = ev.getY();
615                        if (Math.abs(x - mTouchX) > mTouchSlop ||
616                                Math.abs(y - mTouchY) > mTouchSlop) {
617                            mTouchMode = TOUCH_MODE_DRAGGING;
618                            getParent().requestDisallowInterceptTouchEvent(true);
619                            mTouchX = x;
620                            mTouchY = y;
621                            return true;
622                        }
623                        break;
624                    }
625
626                    case TOUCH_MODE_DRAGGING: {
627                        final float x = ev.getX();
628                        final int thumbScrollRange = getThumbScrollRange();
629                        final float thumbScrollOffset = x - mTouchX;
630                        float dPos;
631                        if (thumbScrollRange != 0) {
632                            dPos = thumbScrollOffset / thumbScrollRange;
633                        } else {
634                            // If the thumb scroll range is empty, just use the
635                            // movement direction to snap on or off.
636                            dPos = thumbScrollOffset > 0 ? 1 : -1;
637                        }
638                        if (ViewUtils.isLayoutRtl(this)) {
639                            dPos = -dPos;
640                        }
641                        final float newPos = constrain(mThumbPosition + dPos, 0, 1);
642                        if (newPos != mThumbPosition) {
643                            mTouchX = x;
644                            setThumbPosition(newPos);
645                        }
646                        return true;
647                    }
648                }
649                break;
650            }
651
652            case MotionEvent.ACTION_UP:
653            case MotionEvent.ACTION_CANCEL: {
654                if (mTouchMode == TOUCH_MODE_DRAGGING) {
655                    stopDrag(ev);
656                    // Allow super class to handle pressed state, etc.
657                    super.onTouchEvent(ev);
658                    return true;
659                }
660                mTouchMode = TOUCH_MODE_IDLE;
661                mVelocityTracker.clear();
662                break;
663            }
664        }
665
666        return super.onTouchEvent(ev);
667    }
668
669    private void cancelSuperTouch(MotionEvent ev) {
670        MotionEvent cancel = MotionEvent.obtain(ev);
671        cancel.setAction(MotionEvent.ACTION_CANCEL);
672        super.onTouchEvent(cancel);
673        cancel.recycle();
674    }
675
676    /**
677     * Called from onTouchEvent to end a drag operation.
678     *
679     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
680     */
681    private void stopDrag(MotionEvent ev) {
682        mTouchMode = TOUCH_MODE_IDLE;
683
684        // Commit the change if the event is up and not canceled and the switch
685        // has not been disabled during the drag.
686        final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
687        final boolean oldState = isChecked();
688        final boolean newState;
689        if (commitChange) {
690            mVelocityTracker.computeCurrentVelocity(1000);
691            final float xvel = mVelocityTracker.getXVelocity();
692            if (Math.abs(xvel) > mMinFlingVelocity) {
693                newState = ViewUtils.isLayoutRtl(this) ? (xvel < 0) : (xvel > 0);
694            } else {
695                newState = getTargetCheckedState();
696            }
697        } else {
698            newState = oldState;
699        }
700
701        if (newState != oldState) {
702            playSoundEffect(SoundEffectConstants.CLICK);
703            setChecked(newState);
704        }
705        cancelSuperTouch(ev);
706    }
707
708    private void animateThumbToCheckedState(boolean newCheckedState) {
709        final float startPosition = mThumbPosition;
710        final float targetPosition = newCheckedState ? 1 : 0;
711        final float diff = targetPosition - startPosition;
712
713        mPositionAnimator = new Animation() {
714            @Override
715            protected void applyTransformation(float interpolatedTime, Transformation t) {
716                setThumbPosition(startPosition + (diff * interpolatedTime));
717            }
718        };
719        mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
720        startAnimation(mPositionAnimator);
721    }
722
723    private void cancelPositionAnimator() {
724        if (mPositionAnimator != null) {
725            clearAnimation();
726            mPositionAnimator = null;
727        }
728    }
729
730    private boolean getTargetCheckedState() {
731        return mThumbPosition > 0.5f;
732    }
733
734    /**
735     * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
736     *
737     * @param position new position between [0,1]
738     */
739    private void setThumbPosition(float position) {
740        mThumbPosition = position;
741        invalidate();
742    }
743
744    @Override
745    public void toggle() {
746        setChecked(!isChecked());
747    }
748
749    @Override
750    public void setChecked(boolean checked) {
751        super.setChecked(checked);
752
753        // Calling the super method may result in setChecked() getting called
754        // recursively with a different value, so load the REAL value...
755        checked = isChecked();
756
757        if (getWindowToken() != null) {
758            animateThumbToCheckedState(checked);
759        } else {
760            // Immediately move the thumb to the new position.
761            cancelPositionAnimator();
762            setThumbPosition(checked ? 1 : 0);
763        }
764    }
765
766    @Override
767    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
768        super.onLayout(changed, left, top, right, bottom);
769
770        int opticalInsetLeft = 0;
771        int opticalInsetRight = 0;
772        if (mThumbDrawable != null) {
773            final Rect trackPadding = mTempRect;
774            if (mTrackDrawable != null) {
775                mTrackDrawable.getPadding(trackPadding);
776            } else {
777                trackPadding.setEmpty();
778            }
779
780            opticalInsetLeft = 0;
781            opticalInsetRight = 0;
782        }
783
784        final int switchRight;
785        final int switchLeft;
786        if (ViewUtils.isLayoutRtl(this)) {
787            switchLeft = getPaddingLeft() + opticalInsetLeft;
788            switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
789        } else {
790            switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
791            switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
792        }
793
794        final int switchTop;
795        final int switchBottom;
796        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
797            default:
798            case Gravity.TOP:
799                switchTop = getPaddingTop();
800                switchBottom = switchTop + mSwitchHeight;
801                break;
802
803            case Gravity.CENTER_VERTICAL:
804                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
805                        mSwitchHeight / 2;
806                switchBottom = switchTop + mSwitchHeight;
807                break;
808
809            case Gravity.BOTTOM:
810                switchBottom = getHeight() - getPaddingBottom();
811                switchTop = switchBottom - mSwitchHeight;
812                break;
813        }
814
815        mSwitchLeft = switchLeft;
816        mSwitchTop = switchTop;
817        mSwitchBottom = switchBottom;
818        mSwitchRight = switchRight;
819    }
820
821    @Override
822    public void draw(Canvas c) {
823        final Rect padding = mTempRect;
824        final int switchLeft = mSwitchLeft;
825        final int switchTop = mSwitchTop;
826        final int switchRight = mSwitchRight;
827        final int switchBottom = mSwitchBottom;
828
829        int thumbInitialLeft = switchLeft + getThumbOffset();
830
831        // Layout the track.
832        if (mTrackDrawable != null) {
833            mTrackDrawable.getPadding(padding);
834
835            // Adjust thumb position for track padding.
836            thumbInitialLeft += padding.left;
837
838            // If necessary, offset by the optical insets of the thumb asset.
839            int trackLeft = switchLeft;
840            int trackTop = switchTop;
841            int trackRight = switchRight;
842            int trackBottom = switchBottom;
843            mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
844        }
845
846        // Layout the thumb.
847        if (mThumbDrawable != null) {
848            mThumbDrawable.getPadding(padding);
849
850            final int thumbLeft = thumbInitialLeft - padding.left;
851            final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
852            mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
853
854            final Drawable background = getBackground();
855            if (background != null) {
856                DrawableCompat.setHotspotBounds(background, thumbLeft, switchTop,
857                        thumbRight, switchBottom);
858            }
859        }
860
861        // Draw the background.
862        super.draw(c);
863    }
864
865    @Override
866    protected void onDraw(Canvas canvas) {
867        super.onDraw(canvas);
868
869        final Rect padding = mTempRect;
870        final Drawable trackDrawable = mTrackDrawable;
871        if (trackDrawable != null) {
872            trackDrawable.getPadding(padding);
873        } else {
874            padding.setEmpty();
875        }
876
877        final int switchTop = mSwitchTop;
878        final int switchBottom = mSwitchBottom;
879        final int switchInnerTop = switchTop + padding.top;
880        final int switchInnerBottom = switchBottom - padding.bottom;
881
882        final Drawable thumbDrawable = mThumbDrawable;
883        if (trackDrawable != null) {
884            trackDrawable.draw(canvas);
885        }
886
887        final int saveCount = canvas.save();
888
889        if (thumbDrawable != null) {
890            thumbDrawable.draw(canvas);
891        }
892
893        final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
894        if (switchText != null) {
895            final int drawableState[] = getDrawableState();
896            if (mTextColors != null) {
897                mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
898            }
899            mTextPaint.drawableState = drawableState;
900
901            final int cX;
902            if (thumbDrawable != null) {
903                final Rect bounds = thumbDrawable.getBounds();
904                cX = bounds.left + bounds.right;
905            } else {
906                cX = getWidth();
907            }
908
909            final int left = cX / 2 - switchText.getWidth() / 2;
910            final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
911            canvas.translate(left, top);
912            switchText.draw(canvas);
913        }
914
915        canvas.restoreToCount(saveCount);
916    }
917
918    @Override
919    public int getCompoundPaddingLeft() {
920        if (!ViewUtils.isLayoutRtl(this)) {
921            return super.getCompoundPaddingLeft();
922        }
923        int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
924        if (!TextUtils.isEmpty(getText())) {
925            padding += mSwitchPadding;
926        }
927        return padding;
928    }
929
930    @Override
931    public int getCompoundPaddingRight() {
932        if (ViewUtils.isLayoutRtl(this)) {
933            return super.getCompoundPaddingRight();
934        }
935        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
936        if (!TextUtils.isEmpty(getText())) {
937            padding += mSwitchPadding;
938        }
939        return padding;
940    }
941
942    /**
943     * Translates thumb position to offset according to current RTL setting and
944     * thumb scroll range. Accounts for both track and thumb padding.
945     *
946     * @return thumb offset
947     */
948    private int getThumbOffset() {
949        final float thumbPosition;
950        if (ViewUtils.isLayoutRtl(this)) {
951            thumbPosition = 1 - mThumbPosition;
952        } else {
953            thumbPosition = mThumbPosition;
954        }
955        return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
956    }
957
958    private int getThumbScrollRange() {
959        if (mTrackDrawable != null) {
960            final Rect padding = mTempRect;
961            mTrackDrawable.getPadding(padding);
962            return mSwitchWidth - mThumbWidth - padding.left - padding.right;
963        } else {
964            return 0;
965        }
966    }
967
968    @Override
969    protected int[] onCreateDrawableState(int extraSpace) {
970        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
971        if (isChecked()) {
972            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
973        }
974        return drawableState;
975    }
976
977    @Override
978    protected void drawableStateChanged() {
979        super.drawableStateChanged();
980
981        final int[] myDrawableState = getDrawableState();
982
983        if (mThumbDrawable != null) {
984            mThumbDrawable.setState(myDrawableState);
985        }
986
987        if (mTrackDrawable != null) {
988            mTrackDrawable.setState(myDrawableState);
989        }
990
991        invalidate();
992    }
993
994    @Override
995    public void drawableHotspotChanged(float x, float y) {
996        if (Build.VERSION.SDK_INT >= 21) {
997            super.drawableHotspotChanged(x, y);
998        }
999
1000        if (mThumbDrawable != null) {
1001            DrawableCompat.setHotspot(mThumbDrawable, x, y);
1002        }
1003
1004        if (mTrackDrawable != null) {
1005            DrawableCompat.setHotspot(mTrackDrawable, x, y);
1006        }
1007    }
1008
1009    @Override
1010    protected boolean verifyDrawable(Drawable who) {
1011        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1012    }
1013
1014    @Override
1015    public void jumpDrawablesToCurrentState() {
1016        if (Build.VERSION.SDK_INT >= 11) {
1017            super.jumpDrawablesToCurrentState();
1018
1019            if (mThumbDrawable != null) {
1020                mThumbDrawable.jumpToCurrentState();
1021            }
1022
1023            if (mTrackDrawable != null) {
1024                mTrackDrawable.jumpToCurrentState();
1025            }
1026
1027            if (mPositionAnimator != null && mPositionAnimator.hasStarted() &&
1028                    !mPositionAnimator.hasEnded()) {
1029                clearAnimation();
1030                mPositionAnimator = null;
1031            }
1032        }
1033    }
1034
1035    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1036    @Override
1037    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1038        super.onInitializeAccessibilityEvent(event);
1039        event.setClassName(SwitchCompat.class.getName());
1040    }
1041
1042    @Override
1043    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1044        if (Build.VERSION.SDK_INT >= 14) {
1045            super.onInitializeAccessibilityNodeInfo(info);
1046            info.setClassName(SwitchCompat.class.getName());
1047            CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1048            if (!TextUtils.isEmpty(switchText)) {
1049                CharSequence oldText = info.getText();
1050                if (TextUtils.isEmpty(oldText)) {
1051                    info.setText(switchText);
1052                } else {
1053                    StringBuilder newText = new StringBuilder();
1054                    newText.append(oldText).append(' ').append(switchText);
1055                    info.setText(newText);
1056                }
1057            }
1058        }
1059    }
1060
1061    /**
1062     * Taken from android.util.MathUtils
1063     */
1064    private static float constrain(float amount, float low, float high) {
1065        return amount < low ? low : (amount > high ? high : amount);
1066    }
1067}