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