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