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