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