• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.graphics.Typeface;
27 import android.graphics.drawable.Drawable;
28 import android.text.Layout;
29 import android.text.StaticLayout;
30 import android.text.TextPaint;
31 import android.text.TextUtils;
32 import android.text.method.AllCapsTransformationMethod;
33 import android.text.method.TransformationMethod2;
34 import android.util.AttributeSet;
35 import android.view.Gravity;
36 import android.view.MotionEvent;
37 import android.view.VelocityTracker;
38 import android.view.ViewConfiguration;
39 import android.view.accessibility.AccessibilityEvent;
40 import android.view.accessibility.AccessibilityNodeInfo;
41 
42 import 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  */
68 public 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      */
Switch(Context context)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      */
Switch(Context context, AttributeSet attrs)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      */
Switch(Context context, AttributeSet attrs, int defStyle)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      */
setSwitchTextAppearance(Context context, int resid)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 
setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex)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      */
setSwitchTypeface(Typeface tf, int style)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      */
setSwitchTypeface(Typeface tf)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      */
setSwitchPadding(int pixels)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      */
getSwitchPadding()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      */
setSwitchMinWidth(int pixels)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      */
getSwitchMinWidth()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      */
setThumbTextPadding(int pixels)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      */
getThumbTextPadding()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      */
setTrackDrawable(Drawable track)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      */
setTrackResource(int resId)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      */
getTrackDrawable()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      */
setThumbDrawable(Drawable thumb)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      */
setThumbResource(int resId)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      */
getThumbDrawable()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      */
getTextOn()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      */
setTextOn(CharSequence textOn)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      */
getTextOff()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      */
setTextOff(CharSequence textOff)476     public void setTextOff(CharSequence textOff) {
477         mTextOff = textOff;
478         requestLayout();
479     }
480 
481     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onPopulateAccessibilityEvent(AccessibilityEvent event)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 
makeLayout(CharSequence text)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      */
hitThumb(float x, float y)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
onTouchEvent(MotionEvent ev)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 
cancelSuperTouch(MotionEvent ev)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      */
stopDrag(MotionEvent ev)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 
animateThumbToCheckedState(boolean newCheckedState)641     private void animateThumbToCheckedState(boolean newCheckedState) {
642         // TODO animate!
643         //float targetPos = newCheckedState ? 0 : getThumbScrollRange();
644         //mThumbPosition = targetPos;
645         setChecked(newCheckedState);
646     }
647 
getTargetCheckedState()648     private boolean getTargetCheckedState() {
649         if (isLayoutRtl()) {
650             return mThumbPosition <= getThumbScrollRange() / 2;
651         } else {
652             return mThumbPosition >= getThumbScrollRange() / 2;
653         }
654     }
655 
setThumbPosition(boolean checked)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
setChecked(boolean checked)665     public void setChecked(boolean checked) {
666         super.setChecked(checked);
667         setThumbPosition(isChecked());
668         invalidate();
669     }
670 
671     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)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
onDraw(Canvas canvas)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
getCompoundPaddingLeft()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
getCompoundPaddingRight()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 
getThumbScrollRange()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
onCreateDrawableState(int extraSpace)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
drawableStateChanged()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
verifyDrawable(Drawable who)818     protected boolean verifyDrawable(Drawable who) {
819         return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
820     }
821 
822     @Override
jumpDrawablesToCurrentState()823     public void jumpDrawablesToCurrentState() {
824         super.jumpDrawablesToCurrentState();
825         mThumbDrawable.jumpToCurrentState();
826         mTrackDrawable.jumpToCurrentState();
827     }
828 
829     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)830     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
831         super.onInitializeAccessibilityEvent(event);
832         event.setClassName(Switch.class.getName());
833     }
834 
835     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)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