SwitchCompat.java revision 5dc563e669eb337639a8b0b9e4e28cd5ea08fbea
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.Rect;
27import android.graphics.Region;
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.DrawableUtils;
37import android.support.v7.internal.widget.TintManager;
38import android.support.v7.internal.widget.TintTypedArray;
39import android.support.v7.internal.widget.ViewUtils;
40import android.text.Layout;
41import android.text.StaticLayout;
42import android.text.TextPaint;
43import android.text.TextUtils;
44import android.text.method.TransformationMethod;
45import android.util.AttributeSet;
46import android.view.Gravity;
47import android.view.MotionEvent;
48import android.view.SoundEffectConstants;
49import android.view.VelocityTracker;
50import android.view.ViewConfiguration;
51import android.view.accessibility.AccessibilityEvent;
52import android.view.accessibility.AccessibilityNodeInfo;
53import android.view.animation.Animation;
54import android.view.animation.Transformation;
55import android.widget.CompoundButton;
56
57/**
58 * SwitchCompat is a version of the Switch widget which on devices back to API v7. It does not
59 * make any attempt to use the platform provided widget on those devices which it is available
60 * normally.
61 * <p>
62 * A Switch is a two-state toggle switch widget that can select between two
63 * options. The user may drag the "thumb" back and forth to choose the selected option,
64 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
65 * property controls the text displayed in the label for the switch, whereas the
66 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
67 * controls the text on the thumb. Similarly, the
68 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
69 * setTypeface() methods control the typeface and style of label text, whereas the
70 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
71 * the related seSwitchTypeface() methods control that of the thumb.
72 */
73public class SwitchCompat extends CompoundButton {
74    private static final int THUMB_ANIMATION_DURATION = 250;
75
76    private static final int TOUCH_MODE_IDLE = 0;
77    private static final int TOUCH_MODE_DOWN = 1;
78    private static final int TOUCH_MODE_DRAGGING = 2;
79
80    // We force the accessibility events to have a class name of Switch, since screen readers
81    // already know how to handle their events
82    private static final String ACCESSIBILITY_EVENT_CLASS_NAME = "android.widget.Switch";
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 ThumbAnimation 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        if (mThumbDrawable != null) {
196            mThumbDrawable.setCallback(this);
197        }
198        mTrackDrawable = a.getDrawable(R.styleable.SwitchCompat_track);
199        if (mTrackDrawable != null) {
200            mTrackDrawable.setCallback(this);
201        }
202        mTextOn = a.getText(R.styleable.SwitchCompat_android_textOn);
203        mTextOff = a.getText(R.styleable.SwitchCompat_android_textOff);
204        mShowText = a.getBoolean(R.styleable.SwitchCompat_showText, true);
205        mThumbTextPadding = a.getDimensionPixelSize(
206                R.styleable.SwitchCompat_thumbTextPadding, 0);
207        mSwitchMinWidth = a.getDimensionPixelSize(
208                R.styleable.SwitchCompat_switchMinWidth, 0);
209        mSwitchPadding = a.getDimensionPixelSize(
210                R.styleable.SwitchCompat_switchPadding, 0);
211        mSplitTrack = a.getBoolean(R.styleable.SwitchCompat_splitTrack, false);
212
213        final int appearance = a.getResourceId(
214                R.styleable.SwitchCompat_switchTextAppearance, 0);
215        if (appearance != 0) {
216            setSwitchTextAppearance(context, appearance);
217        }
218
219        mTintManager = a.getTintManager();
220
221        a.recycle();
222
223        final ViewConfiguration config = ViewConfiguration.get(context);
224        mTouchSlop = config.getScaledTouchSlop();
225        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
226
227        // Refresh display with current params
228        refreshDrawableState();
229        setChecked(isChecked());
230    }
231
232    /**
233     * Sets the switch text color, size, style, hint color, and highlight color
234     * from the specified TextAppearance resource.
235     */
236    public void setSwitchTextAppearance(Context context, int resid) {
237        TypedArray appearance = context.obtainStyledAttributes(resid, R.styleable.TextAppearance);
238
239        ColorStateList colors;
240        int ts;
241
242        colors = appearance.getColorStateList(R.styleable.TextAppearance_android_textColor);
243        if (colors != null) {
244            mTextColors = colors;
245        } else {
246            // If no color set in TextAppearance, default to the view's textColor
247            mTextColors = getTextColors();
248        }
249
250        ts = appearance.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0);
251        if (ts != 0) {
252            if (ts != mTextPaint.getTextSize()) {
253                mTextPaint.setTextSize(ts);
254                requestLayout();
255            }
256        }
257
258        int typefaceIndex, styleIndex;
259        typefaceIndex = appearance.getInt(R.styleable.TextAppearance_android_typeface, -1);
260        styleIndex = appearance.getInt(R.styleable.TextAppearance_android_textStyle, -1);
261
262        setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
263
264        boolean allCaps = appearance.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
265        if (allCaps) {
266            mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
267        } else {
268            mSwitchTransformationMethod = null;
269        }
270
271        appearance.recycle();
272    }
273
274    private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
275        Typeface tf = null;
276        switch (typefaceIndex) {
277            case SANS:
278                tf = Typeface.SANS_SERIF;
279                break;
280
281            case SERIF:
282                tf = Typeface.SERIF;
283                break;
284
285            case MONOSPACE:
286                tf = Typeface.MONOSPACE;
287                break;
288        }
289
290        setSwitchTypeface(tf, styleIndex);
291    }
292
293    /**
294     * Sets the typeface and style in which the text should be displayed on the
295     * switch, and turns on the fake bold and italic bits in the Paint if the
296     * Typeface that you provided does not have all the bits in the
297     * style that you specified.
298     */
299    public void setSwitchTypeface(Typeface tf, int style) {
300        if (style > 0) {
301            if (tf == null) {
302                tf = Typeface.defaultFromStyle(style);
303            } else {
304                tf = Typeface.create(tf, style);
305            }
306
307            setSwitchTypeface(tf);
308            // now compute what (if any) algorithmic styling is needed
309            int typefaceStyle = tf != null ? tf.getStyle() : 0;
310            int need = style & ~typefaceStyle;
311            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
312            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
313        } else {
314            mTextPaint.setFakeBoldText(false);
315            mTextPaint.setTextSkewX(0);
316            setSwitchTypeface(tf);
317        }
318    }
319
320    /**
321     * Sets the typeface in which the text should be displayed on the switch.
322     * Note that not all Typeface families actually have bold and italic
323     * variants, so you may need to use
324     * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
325     * that you actually want.
326     */
327    public void setSwitchTypeface(Typeface tf) {
328        if (mTextPaint.getTypeface() != tf) {
329            mTextPaint.setTypeface(tf);
330
331            requestLayout();
332            invalidate();
333        }
334    }
335
336    /**
337     * Set the amount of horizontal padding between the switch and the associated text.
338     *
339     * @param pixels Amount of padding in pixels
340     */
341    public void setSwitchPadding(int pixels) {
342        mSwitchPadding = pixels;
343        requestLayout();
344    }
345
346    /**
347     * Get the amount of horizontal padding between the switch and the associated text.
348     *
349     * @return Amount of padding in pixels
350     */
351    public int getSwitchPadding() {
352        return mSwitchPadding;
353    }
354
355    /**
356     * Set the minimum width of the switch in pixels. The switch's width will be the maximum
357     * of this value and its measured width as determined by the switch drawables and text used.
358     *
359     * @param pixels Minimum width of the switch in pixels
360     */
361    public void setSwitchMinWidth(int pixels) {
362        mSwitchMinWidth = pixels;
363        requestLayout();
364    }
365
366    /**
367     * Get the minimum width of the switch in pixels. The switch's width will be the maximum
368     * of this value and its measured width as determined by the switch drawables and text used.
369     *
370     * @return Minimum width of the switch in pixels
371     */
372    public int getSwitchMinWidth() {
373        return mSwitchMinWidth;
374    }
375
376    /**
377     * Set the horizontal padding around the text drawn on the switch itself.
378     *
379     * @param pixels Horizontal padding for switch thumb text in pixels
380     */
381    public void setThumbTextPadding(int pixels) {
382        mThumbTextPadding = pixels;
383        requestLayout();
384    }
385
386    /**
387     * Get the horizontal padding around the text drawn on the switch itself.
388     *
389     * @return Horizontal padding for switch thumb text in pixels
390     */
391    public int getThumbTextPadding() {
392        return mThumbTextPadding;
393    }
394
395    /**
396     * Set the drawable used for the track that the switch slides within.
397     *
398     * @param track Track drawable
399     */
400    public void setTrackDrawable(Drawable track) {
401        mTrackDrawable = track;
402        requestLayout();
403    }
404
405    /**
406     * Set the drawable used for the track that the switch slides within.
407     *
408     * @param resId Resource ID of a track drawable
409     */
410    public void setTrackResource(int resId) {
411        setTrackDrawable(mTintManager.getDrawable(resId));
412    }
413
414    /**
415     * Get the drawable used for the track that the switch slides within.
416     *
417     * @return Track drawable
418     */
419    public Drawable getTrackDrawable() {
420        return mTrackDrawable;
421    }
422
423    /**
424     * Set the drawable used for the switch "thumb" - the piece that the user
425     * can physically touch and drag along the track.
426     *
427     * @param thumb Thumb drawable
428     */
429    public void setThumbDrawable(Drawable thumb) {
430        mThumbDrawable = thumb;
431        requestLayout();
432    }
433
434    /**
435     * Set the drawable used for the switch "thumb" - the piece that the user
436     * can physically touch and drag along the track.
437     *
438     * @param resId Resource ID of a thumb drawable
439     */
440    public void setThumbResource(int resId) {
441        setThumbDrawable(mTintManager.getDrawable(resId));
442    }
443
444    /**
445     * Get the drawable used for the switch "thumb" - the piece that the user
446     * can physically touch and drag along the track.
447     *
448     * @return Thumb drawable
449     */
450    public Drawable getThumbDrawable() {
451        return mThumbDrawable;
452    }
453
454    /**
455     * Specifies whether the track should be split by the thumb. When true,
456     * the thumb's optical bounds will be clipped out of the track drawable,
457     * then the thumb will be drawn into the resulting gap.
458     *
459     * @param splitTrack Whether the track should be split by the thumb
460     */
461    public void setSplitTrack(boolean splitTrack) {
462        mSplitTrack = splitTrack;
463        invalidate();
464    }
465
466    /**
467     * Returns whether the track should be split by the thumb.
468     */
469    public boolean getSplitTrack() {
470        return mSplitTrack;
471    }
472
473    /**
474     * Returns the text displayed when the button is in the checked state.
475     */
476    public CharSequence getTextOn() {
477        return mTextOn;
478    }
479
480    /**
481     * Sets the text displayed when the button is in the checked state.
482     */
483    public void setTextOn(CharSequence textOn) {
484        mTextOn = textOn;
485        requestLayout();
486    }
487
488    /**
489     * Returns the text displayed when the button is not in the checked state.
490     */
491    public CharSequence getTextOff() {
492        return mTextOff;
493    }
494
495    /**
496     * Sets the text displayed when the button is not in the checked state.
497     */
498    public void setTextOff(CharSequence textOff) {
499        mTextOff = textOff;
500        requestLayout();
501    }
502
503    /**
504     * Sets whether the on/off text should be displayed.
505     *
506     * @param showText {@code true} to display on/off text
507     */
508    public void setShowText(boolean showText) {
509        if (mShowText != showText) {
510            mShowText = showText;
511            requestLayout();
512        }
513    }
514
515    /**
516     * @return whether the on/off text should be displayed
517     */
518    public boolean getShowText() {
519        return mShowText;
520    }
521
522    @Override
523    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
524        if (mShowText) {
525            if (mOnLayout == null) {
526                mOnLayout = makeLayout(mTextOn);
527            }
528
529            if (mOffLayout == null) {
530                mOffLayout = makeLayout(mTextOff);
531            }
532        }
533
534        final Rect padding = mTempRect;
535        final int thumbWidth;
536        final int thumbHeight;
537        if (mThumbDrawable != null) {
538            // Cached thumb width does not include padding.
539            mThumbDrawable.getPadding(padding);
540            thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
541            thumbHeight = mThumbDrawable.getIntrinsicHeight();
542        } else {
543            thumbWidth = 0;
544            thumbHeight = 0;
545        }
546
547        final int maxTextWidth;
548        if (mShowText) {
549            maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
550                    + mThumbTextPadding * 2;
551        } else {
552            maxTextWidth = 0;
553        }
554
555        mThumbWidth = Math.max(maxTextWidth, thumbWidth);
556
557        final int trackHeight;
558        if (mTrackDrawable != null) {
559            mTrackDrawable.getPadding(padding);
560            trackHeight = mTrackDrawable.getIntrinsicHeight();
561        } else {
562            padding.setEmpty();
563            trackHeight = 0;
564        }
565
566        // Adjust left and right padding to ensure there's enough room for the
567        // thumb's padding (when present).
568        int paddingLeft = padding.left;
569        int paddingRight = padding.right;
570        if (mThumbDrawable != null) {
571            final Rect inset = DrawableUtils.getOpticalBounds(mThumbDrawable);
572            paddingLeft = Math.max(paddingLeft, inset.left);
573            paddingRight = Math.max(paddingRight, inset.right);
574        }
575
576        final int switchWidth = Math.max(mSwitchMinWidth,
577                2 * mThumbWidth + paddingLeft + paddingRight);
578        final int switchHeight = Math.max(trackHeight, thumbHeight);
579        mSwitchWidth = switchWidth;
580        mSwitchHeight = switchHeight;
581
582        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
583
584        final int measuredHeight = getMeasuredHeight();
585        if (measuredHeight < switchHeight) {
586            setMeasuredDimension(ViewCompat.getMeasuredWidthAndState(this), switchHeight);
587        }
588    }
589
590    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
591    @Override
592    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
593        super.onPopulateAccessibilityEvent(event);
594
595        final CharSequence text = isChecked() ? mTextOn : mTextOff;
596        if (text != null) {
597            event.getText().add(text);
598        }
599    }
600
601    private Layout makeLayout(CharSequence text) {
602        final CharSequence transformed = (mSwitchTransformationMethod != null)
603                ? mSwitchTransformationMethod.getTransformation(text, this)
604                : text;
605
606        return new StaticLayout(transformed, mTextPaint,
607                transformed != null ?
608                        (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)) : 0,
609                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
610    }
611
612    /**
613     * @return true if (x, y) is within the target area of the switch thumb
614     */
615    private boolean hitThumb(float x, float y) {
616        if (mThumbDrawable == null) {
617            return false;
618        }
619
620        // Relies on mTempRect, MUST be called first!
621        final int thumbOffset = getThumbOffset();
622
623        mThumbDrawable.getPadding(mTempRect);
624        final int thumbTop = mSwitchTop - mTouchSlop;
625        final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
626        final int thumbRight = thumbLeft + mThumbWidth +
627                mTempRect.left + mTempRect.right + mTouchSlop;
628        final int thumbBottom = mSwitchBottom + mTouchSlop;
629        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
630    }
631
632    @Override
633    public boolean onTouchEvent(MotionEvent ev) {
634        mVelocityTracker.addMovement(ev);
635        final int action = MotionEventCompat.getActionMasked(ev);
636        switch (action) {
637            case MotionEvent.ACTION_DOWN: {
638                final float x = ev.getX();
639                final float y = ev.getY();
640                if (isEnabled() && hitThumb(x, y)) {
641                    mTouchMode = TOUCH_MODE_DOWN;
642                    mTouchX = x;
643                    mTouchY = y;
644                }
645                break;
646            }
647
648            case MotionEvent.ACTION_MOVE: {
649                switch (mTouchMode) {
650                    case TOUCH_MODE_IDLE:
651                        // Didn't target the thumb, treat normally.
652                        break;
653
654                    case TOUCH_MODE_DOWN: {
655                        final float x = ev.getX();
656                        final float y = ev.getY();
657                        if (Math.abs(x - mTouchX) > mTouchSlop ||
658                                Math.abs(y - mTouchY) > mTouchSlop) {
659                            mTouchMode = TOUCH_MODE_DRAGGING;
660                            getParent().requestDisallowInterceptTouchEvent(true);
661                            mTouchX = x;
662                            mTouchY = y;
663                            return true;
664                        }
665                        break;
666                    }
667
668                    case TOUCH_MODE_DRAGGING: {
669                        final float x = ev.getX();
670                        final int thumbScrollRange = getThumbScrollRange();
671                        final float thumbScrollOffset = x - mTouchX;
672                        float dPos;
673                        if (thumbScrollRange != 0) {
674                            dPos = thumbScrollOffset / thumbScrollRange;
675                        } else {
676                            // If the thumb scroll range is empty, just use the
677                            // movement direction to snap on or off.
678                            dPos = thumbScrollOffset > 0 ? 1 : -1;
679                        }
680                        if (ViewUtils.isLayoutRtl(this)) {
681                            dPos = -dPos;
682                        }
683                        final float newPos = constrain(mThumbPosition + dPos, 0, 1);
684                        if (newPos != mThumbPosition) {
685                            mTouchX = x;
686                            setThumbPosition(newPos);
687                        }
688                        return true;
689                    }
690                }
691                break;
692            }
693
694            case MotionEvent.ACTION_UP:
695            case MotionEvent.ACTION_CANCEL: {
696                if (mTouchMode == TOUCH_MODE_DRAGGING) {
697                    stopDrag(ev);
698                    // Allow super class to handle pressed state, etc.
699                    super.onTouchEvent(ev);
700                    return true;
701                }
702                mTouchMode = TOUCH_MODE_IDLE;
703                mVelocityTracker.clear();
704                break;
705            }
706        }
707
708        return super.onTouchEvent(ev);
709    }
710
711    private void cancelSuperTouch(MotionEvent ev) {
712        MotionEvent cancel = MotionEvent.obtain(ev);
713        cancel.setAction(MotionEvent.ACTION_CANCEL);
714        super.onTouchEvent(cancel);
715        cancel.recycle();
716    }
717
718    /**
719     * Called from onTouchEvent to end a drag operation.
720     *
721     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
722     */
723    private void stopDrag(MotionEvent ev) {
724        mTouchMode = TOUCH_MODE_IDLE;
725
726        // Commit the change if the event is up and not canceled and the switch
727        // has not been disabled during the drag.
728        final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
729        final boolean oldState = isChecked();
730        final boolean newState;
731        if (commitChange) {
732            mVelocityTracker.computeCurrentVelocity(1000);
733            final float xvel = mVelocityTracker.getXVelocity();
734            if (Math.abs(xvel) > mMinFlingVelocity) {
735                newState = ViewUtils.isLayoutRtl(this) ? (xvel < 0) : (xvel > 0);
736            } else {
737                newState = getTargetCheckedState();
738            }
739        } else {
740            newState = oldState;
741        }
742
743        if (newState != oldState) {
744            playSoundEffect(SoundEffectConstants.CLICK);
745        }
746        // Always call setChecked so that the thumb is moved back to the correct edge
747        setChecked(newState);
748        cancelSuperTouch(ev);
749    }
750
751    private void animateThumbToCheckedState(final boolean newCheckedState) {
752        if (mPositionAnimator != null) {
753            // If there's a current animator running, cancel it
754            cancelPositionAnimator();
755        }
756
757        mPositionAnimator = new ThumbAnimation(mThumbPosition, newCheckedState ? 1f : 0f);
758        mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
759        mPositionAnimator.setAnimationListener(new Animation.AnimationListener() {
760            @Override
761            public void onAnimationStart(Animation animation) {}
762
763            @Override
764            public void onAnimationEnd(Animation animation) {
765                if (mPositionAnimator == animation) {
766                    // If we're still the active animation, ensure the final position
767                    setThumbPosition(newCheckedState ? 1f : 0f);
768                    mPositionAnimator = null;
769                }
770            }
771
772            @Override
773            public void onAnimationRepeat(Animation animation) {}
774        });
775        startAnimation(mPositionAnimator);
776    }
777
778    private void cancelPositionAnimator() {
779        if (mPositionAnimator != null) {
780            clearAnimation();
781            mPositionAnimator = null;
782        }
783    }
784
785    private boolean getTargetCheckedState() {
786        return mThumbPosition > 0.5f;
787    }
788
789    /**
790     * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
791     *
792     * @param position new position between [0,1]
793     */
794    private void setThumbPosition(float position) {
795        mThumbPosition = position;
796        invalidate();
797    }
798
799    @Override
800    public void toggle() {
801        setChecked(!isChecked());
802    }
803
804    @Override
805    public void setChecked(boolean checked) {
806        super.setChecked(checked);
807
808        // Calling the super method may result in setChecked() getting called
809        // recursively with a different value, so load the REAL value...
810        checked = isChecked();
811
812        if (getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown()) {
813            animateThumbToCheckedState(checked);
814        } else {
815            // Immediately move the thumb to the new position.
816            cancelPositionAnimator();
817            setThumbPosition(checked ? 1 : 0);
818        }
819    }
820
821    @Override
822    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
823        super.onLayout(changed, left, top, right, bottom);
824
825        int opticalInsetLeft = 0;
826        int opticalInsetRight = 0;
827        if (mThumbDrawable != null) {
828            final Rect trackPadding = mTempRect;
829            if (mTrackDrawable != null) {
830                mTrackDrawable.getPadding(trackPadding);
831            } else {
832                trackPadding.setEmpty();
833            }
834
835            final Rect insets = DrawableUtils.getOpticalBounds(mThumbDrawable);
836            opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
837            opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
838        }
839
840        final int switchRight;
841        final int switchLeft;
842        if (ViewUtils.isLayoutRtl(this)) {
843            switchLeft = getPaddingLeft() + opticalInsetLeft;
844            switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
845        } else {
846            switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
847            switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
848        }
849
850        final int switchTop;
851        final int switchBottom;
852        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
853            default:
854            case Gravity.TOP:
855                switchTop = getPaddingTop();
856                switchBottom = switchTop + mSwitchHeight;
857                break;
858
859            case Gravity.CENTER_VERTICAL:
860                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
861                        mSwitchHeight / 2;
862                switchBottom = switchTop + mSwitchHeight;
863                break;
864
865            case Gravity.BOTTOM:
866                switchBottom = getHeight() - getPaddingBottom();
867                switchTop = switchBottom - mSwitchHeight;
868                break;
869        }
870
871        mSwitchLeft = switchLeft;
872        mSwitchTop = switchTop;
873        mSwitchBottom = switchBottom;
874        mSwitchRight = switchRight;
875    }
876
877    @Override
878    public void draw(Canvas c) {
879        final Rect padding = mTempRect;
880        final int switchLeft = mSwitchLeft;
881        final int switchTop = mSwitchTop;
882        final int switchRight = mSwitchRight;
883        final int switchBottom = mSwitchBottom;
884
885        int thumbInitialLeft = switchLeft + getThumbOffset();
886
887        final Rect thumbInsets;
888        if (mThumbDrawable != null) {
889            thumbInsets = DrawableUtils.getOpticalBounds(mThumbDrawable);
890        } else {
891            thumbInsets = DrawableUtils.INSETS_NONE;
892        }
893
894        // Layout the track.
895        if (mTrackDrawable != null) {
896            mTrackDrawable.getPadding(padding);
897
898            // Adjust thumb position for track padding.
899            thumbInitialLeft += padding.left;
900
901            // If necessary, offset by the optical insets of the thumb asset.
902            int trackLeft = switchLeft;
903            int trackTop = switchTop;
904            int trackRight = switchRight;
905            int trackBottom = switchBottom;
906            if (thumbInsets != null) {
907                if (thumbInsets.left > padding.left) {
908                    trackLeft += thumbInsets.left - padding.left;
909                }
910                if (thumbInsets.top > padding.top) {
911                    trackTop += thumbInsets.top - padding.top;
912                }
913                if (thumbInsets.right > padding.right) {
914                    trackRight -= thumbInsets.right - padding.right;
915                }
916                if (thumbInsets.bottom > padding.bottom) {
917                    trackBottom -= thumbInsets.bottom - padding.bottom;
918                }
919            }
920            mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
921        }
922
923        // Layout the thumb.
924        if (mThumbDrawable != null) {
925            mThumbDrawable.getPadding(padding);
926
927            final int thumbLeft = thumbInitialLeft - padding.left;
928            final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
929            mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
930
931            final Drawable background = getBackground();
932            if (background != null) {
933                DrawableCompat.setHotspotBounds(background, thumbLeft, switchTop,
934                        thumbRight, switchBottom);
935            }
936        }
937
938        // Draw the background.
939        super.draw(c);
940    }
941
942    @Override
943    protected void onDraw(Canvas canvas) {
944        super.onDraw(canvas);
945
946        final Rect padding = mTempRect;
947        final Drawable trackDrawable = mTrackDrawable;
948        if (trackDrawable != null) {
949            trackDrawable.getPadding(padding);
950        } else {
951            padding.setEmpty();
952        }
953
954        final int switchTop = mSwitchTop;
955        final int switchBottom = mSwitchBottom;
956        final int switchInnerTop = switchTop + padding.top;
957        final int switchInnerBottom = switchBottom - padding.bottom;
958
959        final Drawable thumbDrawable = mThumbDrawable;
960        if (trackDrawable != null) {
961            if (mSplitTrack && thumbDrawable != null) {
962                final Rect insets = DrawableUtils.getOpticalBounds(thumbDrawable);
963                thumbDrawable.copyBounds(padding);
964                padding.left += insets.left;
965                padding.right -= insets.right;
966
967                final int saveCount = canvas.save();
968                canvas.clipRect(padding, Region.Op.DIFFERENCE);
969                trackDrawable.draw(canvas);
970                canvas.restoreToCount(saveCount);
971            } else {
972                trackDrawable.draw(canvas);
973            }
974        }
975
976        final int saveCount = canvas.save();
977
978        if (thumbDrawable != null) {
979            thumbDrawable.draw(canvas);
980        }
981
982        final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
983        if (switchText != null) {
984            final int drawableState[] = getDrawableState();
985            if (mTextColors != null) {
986                mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
987            }
988            mTextPaint.drawableState = drawableState;
989
990            final int cX;
991            if (thumbDrawable != null) {
992                final Rect bounds = thumbDrawable.getBounds();
993                cX = bounds.left + bounds.right;
994            } else {
995                cX = getWidth();
996            }
997
998            final int left = cX / 2 - switchText.getWidth() / 2;
999            final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1000            canvas.translate(left, top);
1001            switchText.draw(canvas);
1002        }
1003
1004        canvas.restoreToCount(saveCount);
1005    }
1006
1007    @Override
1008    public int getCompoundPaddingLeft() {
1009        if (!ViewUtils.isLayoutRtl(this)) {
1010            return super.getCompoundPaddingLeft();
1011        }
1012        int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1013        if (!TextUtils.isEmpty(getText())) {
1014            padding += mSwitchPadding;
1015        }
1016        return padding;
1017    }
1018
1019    @Override
1020    public int getCompoundPaddingRight() {
1021        if (ViewUtils.isLayoutRtl(this)) {
1022            return super.getCompoundPaddingRight();
1023        }
1024        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1025        if (!TextUtils.isEmpty(getText())) {
1026            padding += mSwitchPadding;
1027        }
1028        return padding;
1029    }
1030
1031    /**
1032     * Translates thumb position to offset according to current RTL setting and
1033     * thumb scroll range. Accounts for both track and thumb padding.
1034     *
1035     * @return thumb offset
1036     */
1037    private int getThumbOffset() {
1038        final float thumbPosition;
1039        if (ViewUtils.isLayoutRtl(this)) {
1040            thumbPosition = 1 - mThumbPosition;
1041        } else {
1042            thumbPosition = mThumbPosition;
1043        }
1044        return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1045    }
1046
1047    private int getThumbScrollRange() {
1048        if (mTrackDrawable != null) {
1049            final Rect padding = mTempRect;
1050            mTrackDrawable.getPadding(padding);
1051
1052            final Rect insets;
1053            if (mThumbDrawable != null) {
1054                insets = DrawableUtils.getOpticalBounds(mThumbDrawable);
1055            } else {
1056                insets = DrawableUtils.INSETS_NONE;
1057            }
1058
1059            return mSwitchWidth - mThumbWidth - padding.left - padding.right
1060                    - insets.left - insets.right;
1061        } else {
1062            return 0;
1063        }
1064    }
1065
1066    @Override
1067    protected int[] onCreateDrawableState(int extraSpace) {
1068        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1069        if (isChecked()) {
1070            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1071        }
1072        return drawableState;
1073    }
1074
1075    @Override
1076    protected void drawableStateChanged() {
1077        super.drawableStateChanged();
1078
1079        final int[] myDrawableState = getDrawableState();
1080
1081        if (mThumbDrawable != null) {
1082            mThumbDrawable.setState(myDrawableState);
1083        }
1084
1085        if (mTrackDrawable != null) {
1086            mTrackDrawable.setState(myDrawableState);
1087        }
1088
1089        invalidate();
1090    }
1091
1092    @Override
1093    public void drawableHotspotChanged(float x, float y) {
1094        if (Build.VERSION.SDK_INT >= 21) {
1095            super.drawableHotspotChanged(x, y);
1096        }
1097
1098        if (mThumbDrawable != null) {
1099            DrawableCompat.setHotspot(mThumbDrawable, x, y);
1100        }
1101
1102        if (mTrackDrawable != null) {
1103            DrawableCompat.setHotspot(mTrackDrawable, x, y);
1104        }
1105    }
1106
1107    @Override
1108    protected boolean verifyDrawable(Drawable who) {
1109        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1110    }
1111
1112    @Override
1113    public void jumpDrawablesToCurrentState() {
1114        if (Build.VERSION.SDK_INT >= 11) {
1115            super.jumpDrawablesToCurrentState();
1116
1117            if (mThumbDrawable != null) {
1118                mThumbDrawable.jumpToCurrentState();
1119            }
1120
1121            if (mTrackDrawable != null) {
1122                mTrackDrawable.jumpToCurrentState();
1123            }
1124
1125            cancelPositionAnimator();
1126        }
1127    }
1128
1129    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1130    @Override
1131    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1132        super.onInitializeAccessibilityEvent(event);
1133        event.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME);
1134    }
1135
1136    @Override
1137    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1138        if (Build.VERSION.SDK_INT >= 14) {
1139            super.onInitializeAccessibilityNodeInfo(info);
1140            info.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME);
1141            CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1142            if (!TextUtils.isEmpty(switchText)) {
1143                CharSequence oldText = info.getText();
1144                if (TextUtils.isEmpty(oldText)) {
1145                    info.setText(switchText);
1146                } else {
1147                    StringBuilder newText = new StringBuilder();
1148                    newText.append(oldText).append(' ').append(switchText);
1149                    info.setText(newText);
1150                }
1151            }
1152        }
1153    }
1154
1155    /**
1156     * Taken from android.util.MathUtils
1157     */
1158    private static float constrain(float amount, float low, float high) {
1159        return amount < low ? low : (amount > high ? high : amount);
1160    }
1161
1162    private class ThumbAnimation extends Animation {
1163        final float mStartPosition;
1164        final float mEndPosition;
1165        final float mDiff;
1166
1167        private ThumbAnimation(float startPosition, float endPosition) {
1168            mStartPosition = startPosition;
1169            mEndPosition = endPosition;
1170            mDiff = endPosition - startPosition;
1171        }
1172
1173        @Override
1174        protected void applyTransformation(float interpolatedTime, Transformation t) {
1175            setThumbPosition(mStartPosition + (mDiff * interpolatedTime));
1176        }
1177    }
1178}