Switch.java revision aa0980afab57a6aebb06e70f60e92511708fa5af
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.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.graphics.drawable.Drawable;
28import android.text.Layout;
29import android.text.StaticLayout;
30import android.text.TextPaint;
31import android.text.TextUtils;
32import android.text.method.AllCapsTransformationMethod;
33import android.text.method.TransformationMethod2;
34import android.util.AttributeSet;
35import android.view.Gravity;
36import android.view.MotionEvent;
37import android.view.VelocityTracker;
38import android.view.ViewConfiguration;
39import android.view.accessibility.AccessibilityEvent;
40import android.view.accessibility.AccessibilityNodeInfo;
41
42import com.android.internal.R;
43
44/**
45 * A Switch is a two-state toggle switch widget that can select between two
46 * options. The user may drag the "thumb" back and forth to choose the selected option,
47 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
48 * property controls the text displayed in the label for the switch, whereas the
49 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
50 * controls the text on the thumb. Similarly, the
51 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
52 * setTypeface() methods control the typeface and style of label text, whereas the
53 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
54 * the related seSwitchTypeface() methods control that of the thumb.
55 *
56 */
57public class Switch extends CompoundButton {
58    private static final int TOUCH_MODE_IDLE = 0;
59    private static final int TOUCH_MODE_DOWN = 1;
60    private static final int TOUCH_MODE_DRAGGING = 2;
61
62    // Enum for the "typeface" XML parameter.
63    private static final int SANS = 1;
64    private static final int SERIF = 2;
65    private static final int MONOSPACE = 3;
66
67    private Drawable mThumbDrawable;
68    private Drawable mTrackDrawable;
69    private int mThumbTextPadding;
70    private int mSwitchMinWidth;
71    private int mSwitchPadding;
72    private CharSequence mTextOn;
73    private CharSequence mTextOff;
74
75    private int mTouchMode;
76    private int mTouchSlop;
77    private float mTouchX;
78    private float mTouchY;
79    private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
80    private int mMinFlingVelocity;
81
82    private float mThumbPosition;
83    private int mSwitchWidth;
84    private int mSwitchHeight;
85    private int mThumbWidth; // Does not include padding
86
87    private int mSwitchLeft;
88    private int mSwitchTop;
89    private int mSwitchRight;
90    private int mSwitchBottom;
91
92    private TextPaint mTextPaint;
93    private ColorStateList mTextColors;
94    private Layout mOnLayout;
95    private Layout mOffLayout;
96    private TransformationMethod2 mSwitchTransformationMethod;
97
98    @SuppressWarnings("hiding")
99    private final Rect mTempRect = new Rect();
100
101    private static final int[] CHECKED_STATE_SET = {
102        R.attr.state_checked
103    };
104
105    /**
106     * Construct a new Switch with default styling.
107     *
108     * @param context The Context that will determine this widget's theming.
109     */
110    public Switch(Context context) {
111        this(context, null);
112    }
113
114    /**
115     * Construct a new Switch with default styling, overriding specific style
116     * attributes as requested.
117     *
118     * @param context The Context that will determine this widget's theming.
119     * @param attrs Specification of attributes that should deviate from default styling.
120     */
121    public Switch(Context context, AttributeSet attrs) {
122        this(context, attrs, com.android.internal.R.attr.switchStyle);
123    }
124
125    /**
126     * Construct a new Switch with a default style determined by the given theme attribute,
127     * overriding specific style attributes as requested.
128     *
129     * @param context The Context that will determine this widget's theming.
130     * @param attrs Specification of attributes that should deviate from the default styling.
131     * @param defStyle An attribute ID within the active theme containing a reference to the
132     *                 default style for this widget. e.g. android.R.attr.switchStyle.
133     */
134    public Switch(Context context, AttributeSet attrs, int defStyle) {
135        super(context, attrs, defStyle);
136
137        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
138        Resources res = getResources();
139        mTextPaint.density = res.getDisplayMetrics().density;
140        mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
141
142        TypedArray a = context.obtainStyledAttributes(attrs,
143                com.android.internal.R.styleable.Switch, defStyle, 0);
144
145        mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
146        mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
147        mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
148        mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
149        mThumbTextPadding = a.getDimensionPixelSize(
150                com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
151        mSwitchMinWidth = a.getDimensionPixelSize(
152                com.android.internal.R.styleable.Switch_switchMinWidth, 0);
153        mSwitchPadding = a.getDimensionPixelSize(
154                com.android.internal.R.styleable.Switch_switchPadding, 0);
155
156        int appearance = a.getResourceId(
157                com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
158        if (appearance != 0) {
159            setSwitchTextAppearance(context, appearance);
160        }
161        a.recycle();
162
163        ViewConfiguration config = ViewConfiguration.get(context);
164        mTouchSlop = config.getScaledTouchSlop();
165        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
166
167        // Refresh display with current params
168        refreshDrawableState();
169        setChecked(isChecked());
170    }
171
172    /**
173     * Sets the switch text color, size, style, hint color, and highlight color
174     * from the specified TextAppearance resource.
175     *
176     * @attr ref android.R.styleable#Switch_switchTextAppearance
177     */
178    public void setSwitchTextAppearance(Context context, int resid) {
179        TypedArray appearance =
180                context.obtainStyledAttributes(resid,
181                        com.android.internal.R.styleable.TextAppearance);
182
183        ColorStateList colors;
184        int ts;
185
186        colors = appearance.getColorStateList(com.android.internal.R.styleable.
187                TextAppearance_textColor);
188        if (colors != null) {
189            mTextColors = colors;
190        } else {
191            // If no color set in TextAppearance, default to the view's textColor
192            mTextColors = getTextColors();
193        }
194
195        ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
196                TextAppearance_textSize, 0);
197        if (ts != 0) {
198            if (ts != mTextPaint.getTextSize()) {
199                mTextPaint.setTextSize(ts);
200                requestLayout();
201            }
202        }
203
204        int typefaceIndex, styleIndex;
205
206        typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
207                TextAppearance_typeface, -1);
208        styleIndex = appearance.getInt(com.android.internal.R.styleable.
209                TextAppearance_textStyle, -1);
210
211        setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
212
213        boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
214                TextAppearance_textAllCaps, false);
215        if (allCaps) {
216            mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
217            mSwitchTransformationMethod.setLengthChangesAllowed(true);
218        } else {
219            mSwitchTransformationMethod = null;
220        }
221
222        appearance.recycle();
223    }
224
225    private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
226        Typeface tf = null;
227        switch (typefaceIndex) {
228            case SANS:
229                tf = Typeface.SANS_SERIF;
230                break;
231
232            case SERIF:
233                tf = Typeface.SERIF;
234                break;
235
236            case MONOSPACE:
237                tf = Typeface.MONOSPACE;
238                break;
239        }
240
241        setSwitchTypeface(tf, styleIndex);
242    }
243
244    /**
245     * Sets the typeface and style in which the text should be displayed on the
246     * switch, and turns on the fake bold and italic bits in the Paint if the
247     * Typeface that you provided does not have all the bits in the
248     * style that you specified.
249     */
250    public void setSwitchTypeface(Typeface tf, int style) {
251        if (style > 0) {
252            if (tf == null) {
253                tf = Typeface.defaultFromStyle(style);
254            } else {
255                tf = Typeface.create(tf, style);
256            }
257
258            setSwitchTypeface(tf);
259            // now compute what (if any) algorithmic styling is needed
260            int typefaceStyle = tf != null ? tf.getStyle() : 0;
261            int need = style & ~typefaceStyle;
262            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
263            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
264        } else {
265            mTextPaint.setFakeBoldText(false);
266            mTextPaint.setTextSkewX(0);
267            setSwitchTypeface(tf);
268        }
269    }
270
271    /**
272     * Sets the typeface in which the text should be displayed on the switch.
273     * Note that not all Typeface families actually have bold and italic
274     * variants, so you may need to use
275     * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
276     * that you actually want.
277     *
278     * @attr ref android.R.styleable#TextView_typeface
279     * @attr ref android.R.styleable#TextView_textStyle
280     */
281    public void setSwitchTypeface(Typeface tf) {
282        if (mTextPaint.getTypeface() != tf) {
283            mTextPaint.setTypeface(tf);
284
285            requestLayout();
286            invalidate();
287        }
288    }
289
290    /**
291     * Set the amount of horizontal padding between the switch and the associated text.
292     *
293     * @param pixels Amount of padding in pixels
294     *
295     * @attr ref android.R.styleable#Switch_switchPadding
296     */
297    public void setSwitchPadding(int pixels) {
298        mSwitchPadding = pixels;
299        requestLayout();
300    }
301
302    /**
303     * Get the amount of horizontal padding between the switch and the associated text.
304     *
305     * @return Amount of padding in pixels
306     *
307     * @attr ref android.R.styleable#Switch_switchPadding
308     */
309    public int getSwitchPadding() {
310        return mSwitchPadding;
311    }
312
313    /**
314     * Set the minimum width of the switch in pixels. The switch's width will be the maximum
315     * of this value and its measured width as determined by the switch drawables and text used.
316     *
317     * @param pixels Minimum width of the switch in pixels
318     *
319     * @attr ref android.R.styleable#Switch_switchMinWidth
320     */
321    public void setSwitchMinWidth(int pixels) {
322        mSwitchMinWidth = pixels;
323        requestLayout();
324    }
325
326    /**
327     * Get the minimum width of the switch in pixels. The switch's width will be the maximum
328     * of this value and its measured width as determined by the switch drawables and text used.
329     *
330     * @return Minimum width of the switch in pixels
331     *
332     * @attr ref android.R.styleable#Switch_switchMinWidth
333     */
334    public int getSwitchMinWidth() {
335        return mSwitchMinWidth;
336    }
337
338    /**
339     * Set the horizontal padding around the text drawn on the switch itself.
340     *
341     * @param pixels Horizontal padding for switch thumb text in pixels
342     *
343     * @attr ref android.R.styleable#Switch_thumbTextPadding
344     */
345    public void setThumbTextPadding(int pixels) {
346        mThumbTextPadding = pixels;
347        requestLayout();
348    }
349
350    /**
351     * Get the horizontal padding around the text drawn on the switch itself.
352     *
353     * @return Horizontal padding for switch thumb text in pixels
354     *
355     * @attr ref android.R.styleable#Switch_thumbTextPadding
356     */
357    public int getThumbTextPadding() {
358        return mThumbTextPadding;
359    }
360
361    /**
362     * Set the drawable used for the track that the switch slides within.
363     *
364     * @param track Track drawable
365     *
366     * @attr ref android.R.styleable#Switch_track
367     */
368    public void setTrackDrawable(Drawable track) {
369        mTrackDrawable = track;
370        requestLayout();
371    }
372
373    /**
374     * Set the drawable used for the track that the switch slides within.
375     *
376     * @param resId Resource ID of a track drawable
377     *
378     * @attr ref android.R.styleable#Switch_track
379     */
380    public void setTrackResource(int resId) {
381        setTrackDrawable(getContext().getResources().getDrawable(resId));
382    }
383
384    /**
385     * Get the drawable used for the track that the switch slides within.
386     *
387     * @return Track drawable
388     *
389     * @attr ref android.R.styleable#Switch_track
390     */
391    public Drawable getTrackDrawable() {
392        return mTrackDrawable;
393    }
394
395    /**
396     * Set the drawable used for the switch "thumb" - the piece that the user
397     * can physically touch and drag along the track.
398     *
399     * @param thumb Thumb drawable
400     *
401     * @attr ref android.R.styleable#Switch_thumb
402     */
403    public void setThumbDrawable(Drawable thumb) {
404        mThumbDrawable = thumb;
405        requestLayout();
406    }
407
408    /**
409     * Set the drawable used for the switch "thumb" - the piece that the user
410     * can physically touch and drag along the track.
411     *
412     * @param resId Resource ID of a thumb drawable
413     *
414     * @attr ref android.R.styleable#Switch_thumb
415     */
416    public void setThumbResource(int resId) {
417        setThumbDrawable(getContext().getResources().getDrawable(resId));
418    }
419
420    /**
421     * Get the drawable used for the switch "thumb" - the piece that the user
422     * can physically touch and drag along the track.
423     *
424     * @return Thumb drawable
425     *
426     * @attr ref android.R.styleable#Switch_thumb
427     */
428    public Drawable getThumbDrawable() {
429        return mThumbDrawable;
430    }
431
432    /**
433     * Returns the text displayed when the button is in the checked state.
434     *
435     * @attr ref android.R.styleable#Switch_textOn
436     */
437    public CharSequence getTextOn() {
438        return mTextOn;
439    }
440
441    /**
442     * Sets the text displayed when the button is in the checked state.
443     *
444     * @attr ref android.R.styleable#Switch_textOn
445     */
446    public void setTextOn(CharSequence textOn) {
447        mTextOn = textOn;
448        requestLayout();
449    }
450
451    /**
452     * Returns the text displayed when the button is not in the checked state.
453     *
454     * @attr ref android.R.styleable#Switch_textOff
455     */
456    public CharSequence getTextOff() {
457        return mTextOff;
458    }
459
460    /**
461     * Sets the text displayed when the button is not in the checked state.
462     *
463     * @attr ref android.R.styleable#Switch_textOff
464     */
465    public void setTextOff(CharSequence textOff) {
466        mTextOff = textOff;
467        requestLayout();
468    }
469
470    @Override
471    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
472        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
473        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
474        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
475        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
476
477
478        if (mOnLayout == null) {
479            mOnLayout = makeLayout(mTextOn);
480        }
481        if (mOffLayout == null) {
482            mOffLayout = makeLayout(mTextOff);
483        }
484
485        mTrackDrawable.getPadding(mTempRect);
486        final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth());
487        final int switchWidth = Math.max(mSwitchMinWidth,
488                maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
489        final int switchHeight = mTrackDrawable.getIntrinsicHeight();
490
491        mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
492
493        switch (widthMode) {
494            case MeasureSpec.AT_MOST:
495                widthSize = Math.min(widthSize, switchWidth);
496                break;
497
498            case MeasureSpec.UNSPECIFIED:
499                widthSize = switchWidth;
500                break;
501
502            case MeasureSpec.EXACTLY:
503                // Just use what we were given
504                break;
505        }
506
507        switch (heightMode) {
508            case MeasureSpec.AT_MOST:
509                heightSize = Math.min(heightSize, switchHeight);
510                break;
511
512            case MeasureSpec.UNSPECIFIED:
513                heightSize = switchHeight;
514                break;
515
516            case MeasureSpec.EXACTLY:
517                // Just use what we were given
518                break;
519        }
520
521        mSwitchWidth = switchWidth;
522        mSwitchHeight = switchHeight;
523
524        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
525        final int measuredHeight = getMeasuredHeight();
526        if (measuredHeight < switchHeight) {
527            setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
528        }
529    }
530
531    @Override
532    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
533        super.onPopulateAccessibilityEvent(event);
534        CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText();
535        if (!TextUtils.isEmpty(text)) {
536            event.getText().add(text);
537        }
538    }
539
540    private Layout makeLayout(CharSequence text) {
541        final CharSequence transformed = (mSwitchTransformationMethod != null)
542                    ? mSwitchTransformationMethod.getTransformation(text, this)
543                    : text;
544
545        return new StaticLayout(transformed, mTextPaint,
546                (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
547                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
548    }
549
550    /**
551     * @return true if (x, y) is within the target area of the switch thumb
552     */
553    private boolean hitThumb(float x, float y) {
554        mThumbDrawable.getPadding(mTempRect);
555        final int thumbTop = mSwitchTop - mTouchSlop;
556        final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
557        final int thumbRight = thumbLeft + mThumbWidth +
558                mTempRect.left + mTempRect.right + mTouchSlop;
559        final int thumbBottom = mSwitchBottom + mTouchSlop;
560        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
561    }
562
563    @Override
564    public boolean onTouchEvent(MotionEvent ev) {
565        mVelocityTracker.addMovement(ev);
566        final int action = ev.getActionMasked();
567        switch (action) {
568            case MotionEvent.ACTION_DOWN: {
569                final float x = ev.getX();
570                final float y = ev.getY();
571                if (isEnabled() && hitThumb(x, y)) {
572                    mTouchMode = TOUCH_MODE_DOWN;
573                    mTouchX = x;
574                    mTouchY = y;
575                }
576                break;
577            }
578
579            case MotionEvent.ACTION_MOVE: {
580                switch (mTouchMode) {
581                    case TOUCH_MODE_IDLE:
582                        // Didn't target the thumb, treat normally.
583                        break;
584
585                    case TOUCH_MODE_DOWN: {
586                        final float x = ev.getX();
587                        final float y = ev.getY();
588                        if (Math.abs(x - mTouchX) > mTouchSlop ||
589                                Math.abs(y - mTouchY) > mTouchSlop) {
590                            mTouchMode = TOUCH_MODE_DRAGGING;
591                            getParent().requestDisallowInterceptTouchEvent(true);
592                            mTouchX = x;
593                            mTouchY = y;
594                            return true;
595                        }
596                        break;
597                    }
598
599                    case TOUCH_MODE_DRAGGING: {
600                        final float x = ev.getX();
601                        final float dx = x - mTouchX;
602                        float newPos = Math.max(0,
603                                Math.min(mThumbPosition + dx, getThumbScrollRange()));
604                        if (newPos != mThumbPosition) {
605                            mThumbPosition = newPos;
606                            mTouchX = x;
607                            invalidate();
608                        }
609                        return true;
610                    }
611                }
612                break;
613            }
614
615            case MotionEvent.ACTION_UP:
616            case MotionEvent.ACTION_CANCEL: {
617                if (mTouchMode == TOUCH_MODE_DRAGGING) {
618                    stopDrag(ev);
619                    return true;
620                }
621                mTouchMode = TOUCH_MODE_IDLE;
622                mVelocityTracker.clear();
623                break;
624            }
625        }
626
627        return super.onTouchEvent(ev);
628    }
629
630    private void cancelSuperTouch(MotionEvent ev) {
631        MotionEvent cancel = MotionEvent.obtain(ev);
632        cancel.setAction(MotionEvent.ACTION_CANCEL);
633        super.onTouchEvent(cancel);
634        cancel.recycle();
635    }
636
637    /**
638     * Called from onTouchEvent to end a drag operation.
639     *
640     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
641     */
642    private void stopDrag(MotionEvent ev) {
643        mTouchMode = TOUCH_MODE_IDLE;
644        // Up and not canceled, also checks the switch has not been disabled during the drag
645        boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
646
647        cancelSuperTouch(ev);
648
649        if (commitChange) {
650            boolean newState;
651            mVelocityTracker.computeCurrentVelocity(1000);
652            float xvel = mVelocityTracker.getXVelocity();
653            if (Math.abs(xvel) > mMinFlingVelocity) {
654                newState = xvel > 0;
655            } else {
656                newState = getTargetCheckedState();
657            }
658            animateThumbToCheckedState(newState);
659        } else {
660            animateThumbToCheckedState(isChecked());
661        }
662    }
663
664    private void animateThumbToCheckedState(boolean newCheckedState) {
665        // TODO animate!
666        //float targetPos = newCheckedState ? 0 : getThumbScrollRange();
667        //mThumbPosition = targetPos;
668        setChecked(newCheckedState);
669    }
670
671    private boolean getTargetCheckedState() {
672        return mThumbPosition >= getThumbScrollRange() / 2;
673    }
674
675    @Override
676    public void setChecked(boolean checked) {
677        super.setChecked(checked);
678        mThumbPosition = checked ? getThumbScrollRange() : 0;
679        invalidate();
680    }
681
682    @Override
683    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
684        super.onLayout(changed, left, top, right, bottom);
685
686        mThumbPosition = isChecked() ? getThumbScrollRange() : 0;
687
688        int switchRight = getWidth() - getPaddingRight();
689        int switchLeft = switchRight - mSwitchWidth;
690        int switchTop = 0;
691        int switchBottom = 0;
692        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
693            default:
694            case Gravity.TOP:
695                switchTop = getPaddingTop();
696                switchBottom = switchTop + mSwitchHeight;
697                break;
698
699            case Gravity.CENTER_VERTICAL:
700                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
701                        mSwitchHeight / 2;
702                switchBottom = switchTop + mSwitchHeight;
703                break;
704
705            case Gravity.BOTTOM:
706                switchBottom = getHeight() - getPaddingBottom();
707                switchTop = switchBottom - mSwitchHeight;
708                break;
709        }
710
711        mSwitchLeft = switchLeft;
712        mSwitchTop = switchTop;
713        mSwitchBottom = switchBottom;
714        mSwitchRight = switchRight;
715    }
716
717    @Override
718    protected void onDraw(Canvas canvas) {
719        super.onDraw(canvas);
720
721        // Draw the switch
722        int switchLeft = mSwitchLeft;
723        int switchTop = mSwitchTop;
724        int switchRight = mSwitchRight;
725        int switchBottom = mSwitchBottom;
726
727        mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
728        mTrackDrawable.draw(canvas);
729
730        canvas.save();
731
732        mTrackDrawable.getPadding(mTempRect);
733        int switchInnerLeft = switchLeft + mTempRect.left;
734        int switchInnerTop = switchTop + mTempRect.top;
735        int switchInnerRight = switchRight - mTempRect.right;
736        int switchInnerBottom = switchBottom - mTempRect.bottom;
737        canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
738
739        mThumbDrawable.getPadding(mTempRect);
740        final int thumbPos = (int) (mThumbPosition + 0.5f);
741        int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
742        int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
743
744        mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
745        mThumbDrawable.draw(canvas);
746
747        // mTextColors should not be null, but just in case
748        if (mTextColors != null) {
749            mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
750                    mTextColors.getDefaultColor()));
751        }
752        mTextPaint.drawableState = getDrawableState();
753
754        Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
755
756        canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2,
757                (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
758        switchText.draw(canvas);
759
760        canvas.restore();
761    }
762
763    @Override
764    public int getCompoundPaddingRight() {
765        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
766        if (!TextUtils.isEmpty(getText())) {
767            padding += mSwitchPadding;
768        }
769        return padding;
770    }
771
772    private int getThumbScrollRange() {
773        if (mTrackDrawable == null) {
774            return 0;
775        }
776        mTrackDrawable.getPadding(mTempRect);
777        return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
778    }
779
780    @Override
781    protected int[] onCreateDrawableState(int extraSpace) {
782        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
783        if (isChecked()) {
784            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
785        }
786        return drawableState;
787    }
788
789    @Override
790    protected void drawableStateChanged() {
791        super.drawableStateChanged();
792
793        int[] myDrawableState = getDrawableState();
794
795        // Set the state of the Drawable
796        // Drawable may be null when checked state is set from XML, from super constructor
797        if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
798        if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
799
800        invalidate();
801    }
802
803    @Override
804    protected boolean verifyDrawable(Drawable who) {
805        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
806    }
807
808    @Override
809    public void jumpDrawablesToCurrentState() {
810        super.jumpDrawablesToCurrentState();
811        mThumbDrawable.jumpToCurrentState();
812        mTrackDrawable.jumpToCurrentState();
813    }
814
815    @Override
816    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
817        super.onInitializeAccessibilityEvent(event);
818        event.setClassName(Switch.class.getName());
819    }
820
821    @Override
822    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
823        super.onInitializeAccessibilityNodeInfo(info);
824        info.setClassName(Switch.class.getName());
825        CharSequence switchText = isChecked() ? mTextOn : mTextOff;
826        if (!TextUtils.isEmpty(switchText)) {
827            CharSequence oldText = info.getText();
828            if (TextUtils.isEmpty(oldText)) {
829                info.setText(switchText);
830            } else {
831                StringBuilder newText = new StringBuilder();
832                newText.append(oldText).append(' ').append(switchText);
833                info.setText(newText);
834            }
835        }
836    }
837}
838