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