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