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