• 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.animation.ObjectAnimator;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.Insets;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.graphics.Typeface;
29 import android.graphics.Region.Op;
30 import android.graphics.drawable.Drawable;
31 import android.text.Layout;
32 import android.text.StaticLayout;
33 import android.text.TextPaint;
34 import android.text.TextUtils;
35 import android.text.method.AllCapsTransformationMethod;
36 import android.text.method.TransformationMethod2;
37 import android.util.AttributeSet;
38 import android.util.FloatProperty;
39 import android.util.MathUtils;
40 import android.view.Gravity;
41 import android.view.MotionEvent;
42 import android.view.SoundEffectConstants;
43 import android.view.VelocityTracker;
44 import android.view.ViewConfiguration;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityNodeInfo;
47 
48 import com.android.internal.R;
49 
50 /**
51  * A Switch is a two-state toggle switch widget that can select between two
52  * options. The user may drag the "thumb" back and forth to choose the selected option,
53  * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
54  * property controls the text displayed in the label for the switch, whereas the
55  * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
56  * controls the text on the thumb. Similarly, the
57  * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
58  * setTypeface() methods control the typeface and style of label text, whereas the
59  * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
60  * the related seSwitchTypeface() methods control that of the thumb.
61  *
62  * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
63  * guide.</p>
64  *
65  * @attr ref android.R.styleable#Switch_textOn
66  * @attr ref android.R.styleable#Switch_textOff
67  * @attr ref android.R.styleable#Switch_switchMinWidth
68  * @attr ref android.R.styleable#Switch_switchPadding
69  * @attr ref android.R.styleable#Switch_switchTextAppearance
70  * @attr ref android.R.styleable#Switch_thumb
71  * @attr ref android.R.styleable#Switch_thumbTextPadding
72  * @attr ref android.R.styleable#Switch_track
73  */
74 public class Switch extends CompoundButton {
75     private static final int THUMB_ANIMATION_DURATION = 250;
76 
77     private static final int TOUCH_MODE_IDLE = 0;
78     private static final int TOUCH_MODE_DOWN = 1;
79     private static final int TOUCH_MODE_DRAGGING = 2;
80 
81     // Enum for the "typeface" XML parameter.
82     private static final int SANS = 1;
83     private static final int SERIF = 2;
84     private static final int MONOSPACE = 3;
85 
86     private Drawable mThumbDrawable;
87     private Drawable mTrackDrawable;
88     private int mThumbTextPadding;
89     private int mSwitchMinWidth;
90     private int mSwitchPadding;
91     private boolean mSplitTrack;
92     private CharSequence mTextOn;
93     private CharSequence mTextOff;
94     private boolean mShowText;
95 
96     private int mTouchMode;
97     private int mTouchSlop;
98     private float mTouchX;
99     private float mTouchY;
100     private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
101     private int mMinFlingVelocity;
102 
103     private float mThumbPosition;
104 
105     /**
106      * Width required to draw the switch track and thumb. Includes padding and
107      * optical bounds for both the track and thumb.
108      */
109     private int mSwitchWidth;
110 
111     /**
112      * Height required to draw the switch track and thumb. Includes padding and
113      * optical bounds for both the track and thumb.
114      */
115     private int mSwitchHeight;
116 
117     /**
118      * Width of the thumb's content region. Does not include padding or
119      * optical bounds.
120      */
121     private int mThumbWidth;
122 
123     /** Left bound for drawing the switch track and thumb. */
124     private int mSwitchLeft;
125 
126     /** Top bound for drawing the switch track and thumb. */
127     private int mSwitchTop;
128 
129     /** Right bound for drawing the switch track and thumb. */
130     private int mSwitchRight;
131 
132     /** Bottom bound for drawing the switch track and thumb. */
133     private int mSwitchBottom;
134 
135     private TextPaint mTextPaint;
136     private ColorStateList mTextColors;
137     private Layout mOnLayout;
138     private Layout mOffLayout;
139     private TransformationMethod2 mSwitchTransformationMethod;
140     private ObjectAnimator mPositionAnimator;
141 
142     @SuppressWarnings("hiding")
143     private final Rect mTempRect = new Rect();
144 
145     private static final int[] CHECKED_STATE_SET = {
146         R.attr.state_checked
147     };
148 
149     /**
150      * Construct a new Switch with default styling.
151      *
152      * @param context The Context that will determine this widget's theming.
153      */
Switch(Context context)154     public Switch(Context context) {
155         this(context, null);
156     }
157 
158     /**
159      * Construct a new Switch with default styling, overriding specific style
160      * attributes as requested.
161      *
162      * @param context The Context that will determine this widget's theming.
163      * @param attrs Specification of attributes that should deviate from default styling.
164      */
Switch(Context context, AttributeSet attrs)165     public Switch(Context context, AttributeSet attrs) {
166         this(context, attrs, com.android.internal.R.attr.switchStyle);
167     }
168 
169     /**
170      * Construct a new Switch with a default style determined by the given theme attribute,
171      * overriding specific style attributes as requested.
172      *
173      * @param context The Context that will determine this widget's theming.
174      * @param attrs Specification of attributes that should deviate from the default styling.
175      * @param defStyleAttr An attribute in the current theme that contains a
176      *        reference to a style resource that supplies default values for
177      *        the view. Can be 0 to not look for defaults.
178      */
Switch(Context context, AttributeSet attrs, int defStyleAttr)179     public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
180         this(context, attrs, defStyleAttr, 0);
181     }
182 
183 
184     /**
185      * Construct a new Switch with a default style determined by the given theme
186      * attribute or style resource, overriding specific style attributes as
187      * requested.
188      *
189      * @param context The Context that will determine this widget's theming.
190      * @param attrs Specification of attributes that should deviate from the
191      *        default styling.
192      * @param defStyleAttr An attribute in the current theme that contains a
193      *        reference to a style resource that supplies default values for
194      *        the view. Can be 0 to not look for defaults.
195      * @param defStyleRes A resource identifier of a style resource that
196      *        supplies default values for the view, used only if
197      *        defStyleAttr is 0 or can not be found in the theme. Can be 0
198      *        to not look for defaults.
199      */
Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)200     public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
201         super(context, attrs, defStyleAttr, defStyleRes);
202 
203         mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
204 
205         final Resources res = getResources();
206         mTextPaint.density = res.getDisplayMetrics().density;
207         mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
208 
209         final TypedArray a = context.obtainStyledAttributes(
210                 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
211         mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
212         if (mThumbDrawable != null) {
213             mThumbDrawable.setCallback(this);
214         }
215         mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
216         if (mTrackDrawable != null) {
217             mTrackDrawable.setCallback(this);
218         }
219         mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
220         mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
221         mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
222         mThumbTextPadding = a.getDimensionPixelSize(
223                 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
224         mSwitchMinWidth = a.getDimensionPixelSize(
225                 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
226         mSwitchPadding = a.getDimensionPixelSize(
227                 com.android.internal.R.styleable.Switch_switchPadding, 0);
228         mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
229 
230         final int appearance = a.getResourceId(
231                 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
232         if (appearance != 0) {
233             setSwitchTextAppearance(context, appearance);
234         }
235         a.recycle();
236 
237         final ViewConfiguration config = ViewConfiguration.get(context);
238         mTouchSlop = config.getScaledTouchSlop();
239         mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
240 
241         // Refresh display with current params
242         refreshDrawableState();
243         setChecked(isChecked());
244     }
245 
246     /**
247      * Sets the switch text color, size, style, hint color, and highlight color
248      * from the specified TextAppearance resource.
249      *
250      * @attr ref android.R.styleable#Switch_switchTextAppearance
251      */
setSwitchTextAppearance(Context context, int resid)252     public void setSwitchTextAppearance(Context context, int resid) {
253         TypedArray appearance =
254                 context.obtainStyledAttributes(resid,
255                         com.android.internal.R.styleable.TextAppearance);
256 
257         ColorStateList colors;
258         int ts;
259 
260         colors = appearance.getColorStateList(com.android.internal.R.styleable.
261                 TextAppearance_textColor);
262         if (colors != null) {
263             mTextColors = colors;
264         } else {
265             // If no color set in TextAppearance, default to the view's textColor
266             mTextColors = getTextColors();
267         }
268 
269         ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
270                 TextAppearance_textSize, 0);
271         if (ts != 0) {
272             if (ts != mTextPaint.getTextSize()) {
273                 mTextPaint.setTextSize(ts);
274                 requestLayout();
275             }
276         }
277 
278         int typefaceIndex, styleIndex;
279 
280         typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
281                 TextAppearance_typeface, -1);
282         styleIndex = appearance.getInt(com.android.internal.R.styleable.
283                 TextAppearance_textStyle, -1);
284 
285         setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
286 
287         boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
288                 TextAppearance_textAllCaps, false);
289         if (allCaps) {
290             mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
291             mSwitchTransformationMethod.setLengthChangesAllowed(true);
292         } else {
293             mSwitchTransformationMethod = null;
294         }
295 
296         appearance.recycle();
297     }
298 
setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex)299     private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
300         Typeface tf = null;
301         switch (typefaceIndex) {
302             case SANS:
303                 tf = Typeface.SANS_SERIF;
304                 break;
305 
306             case SERIF:
307                 tf = Typeface.SERIF;
308                 break;
309 
310             case MONOSPACE:
311                 tf = Typeface.MONOSPACE;
312                 break;
313         }
314 
315         setSwitchTypeface(tf, styleIndex);
316     }
317 
318     /**
319      * Sets the typeface and style in which the text should be displayed on the
320      * switch, and turns on the fake bold and italic bits in the Paint if the
321      * Typeface that you provided does not have all the bits in the
322      * style that you specified.
323      */
setSwitchTypeface(Typeface tf, int style)324     public void setSwitchTypeface(Typeface tf, int style) {
325         if (style > 0) {
326             if (tf == null) {
327                 tf = Typeface.defaultFromStyle(style);
328             } else {
329                 tf = Typeface.create(tf, style);
330             }
331 
332             setSwitchTypeface(tf);
333             // now compute what (if any) algorithmic styling is needed
334             int typefaceStyle = tf != null ? tf.getStyle() : 0;
335             int need = style & ~typefaceStyle;
336             mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
337             mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
338         } else {
339             mTextPaint.setFakeBoldText(false);
340             mTextPaint.setTextSkewX(0);
341             setSwitchTypeface(tf);
342         }
343     }
344 
345     /**
346      * Sets the typeface in which the text should be displayed on the switch.
347      * Note that not all Typeface families actually have bold and italic
348      * variants, so you may need to use
349      * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
350      * that you actually want.
351      *
352      * @attr ref android.R.styleable#TextView_typeface
353      * @attr ref android.R.styleable#TextView_textStyle
354      */
setSwitchTypeface(Typeface tf)355     public void setSwitchTypeface(Typeface tf) {
356         if (mTextPaint.getTypeface() != tf) {
357             mTextPaint.setTypeface(tf);
358 
359             requestLayout();
360             invalidate();
361         }
362     }
363 
364     /**
365      * Set the amount of horizontal padding between the switch and the associated text.
366      *
367      * @param pixels Amount of padding in pixels
368      *
369      * @attr ref android.R.styleable#Switch_switchPadding
370      */
setSwitchPadding(int pixels)371     public void setSwitchPadding(int pixels) {
372         mSwitchPadding = pixels;
373         requestLayout();
374     }
375 
376     /**
377      * Get the amount of horizontal padding between the switch and the associated text.
378      *
379      * @return Amount of padding in pixels
380      *
381      * @attr ref android.R.styleable#Switch_switchPadding
382      */
getSwitchPadding()383     public int getSwitchPadding() {
384         return mSwitchPadding;
385     }
386 
387     /**
388      * Set the minimum width of the switch in pixels. The switch's width will be the maximum
389      * of this value and its measured width as determined by the switch drawables and text used.
390      *
391      * @param pixels Minimum width of the switch in pixels
392      *
393      * @attr ref android.R.styleable#Switch_switchMinWidth
394      */
setSwitchMinWidth(int pixels)395     public void setSwitchMinWidth(int pixels) {
396         mSwitchMinWidth = pixels;
397         requestLayout();
398     }
399 
400     /**
401      * Get the minimum width of the switch in pixels. The switch's width will be the maximum
402      * of this value and its measured width as determined by the switch drawables and text used.
403      *
404      * @return Minimum width of the switch in pixels
405      *
406      * @attr ref android.R.styleable#Switch_switchMinWidth
407      */
getSwitchMinWidth()408     public int getSwitchMinWidth() {
409         return mSwitchMinWidth;
410     }
411 
412     /**
413      * Set the horizontal padding around the text drawn on the switch itself.
414      *
415      * @param pixels Horizontal padding for switch thumb text in pixels
416      *
417      * @attr ref android.R.styleable#Switch_thumbTextPadding
418      */
setThumbTextPadding(int pixels)419     public void setThumbTextPadding(int pixels) {
420         mThumbTextPadding = pixels;
421         requestLayout();
422     }
423 
424     /**
425      * Get the horizontal padding around the text drawn on the switch itself.
426      *
427      * @return Horizontal padding for switch thumb text in pixels
428      *
429      * @attr ref android.R.styleable#Switch_thumbTextPadding
430      */
getThumbTextPadding()431     public int getThumbTextPadding() {
432         return mThumbTextPadding;
433     }
434 
435     /**
436      * Set the drawable used for the track that the switch slides within.
437      *
438      * @param track Track drawable
439      *
440      * @attr ref android.R.styleable#Switch_track
441      */
setTrackDrawable(Drawable track)442     public void setTrackDrawable(Drawable track) {
443         if (mTrackDrawable != null) {
444             mTrackDrawable.setCallback(null);
445         }
446         mTrackDrawable = track;
447         if (track != null) {
448             track.setCallback(this);
449         }
450         requestLayout();
451     }
452 
453     /**
454      * Set the drawable used for the track that the switch slides within.
455      *
456      * @param resId Resource ID of a track drawable
457      *
458      * @attr ref android.R.styleable#Switch_track
459      */
setTrackResource(int resId)460     public void setTrackResource(int resId) {
461         setTrackDrawable(getContext().getDrawable(resId));
462     }
463 
464     /**
465      * Get the drawable used for the track that the switch slides within.
466      *
467      * @return Track drawable
468      *
469      * @attr ref android.R.styleable#Switch_track
470      */
getTrackDrawable()471     public Drawable getTrackDrawable() {
472         return mTrackDrawable;
473     }
474 
475     /**
476      * Set the drawable used for the switch "thumb" - the piece that the user
477      * can physically touch and drag along the track.
478      *
479      * @param thumb Thumb drawable
480      *
481      * @attr ref android.R.styleable#Switch_thumb
482      */
setThumbDrawable(Drawable thumb)483     public void setThumbDrawable(Drawable thumb) {
484         if (mThumbDrawable != null) {
485             mThumbDrawable.setCallback(null);
486         }
487         mThumbDrawable = thumb;
488         if (thumb != null) {
489             thumb.setCallback(this);
490         }
491         requestLayout();
492     }
493 
494     /**
495      * Set the drawable used for the switch "thumb" - the piece that the user
496      * can physically touch and drag along the track.
497      *
498      * @param resId Resource ID of a thumb drawable
499      *
500      * @attr ref android.R.styleable#Switch_thumb
501      */
setThumbResource(int resId)502     public void setThumbResource(int resId) {
503         setThumbDrawable(getContext().getDrawable(resId));
504     }
505 
506     /**
507      * Get the drawable used for the switch "thumb" - the piece that the user
508      * can physically touch and drag along the track.
509      *
510      * @return Thumb drawable
511      *
512      * @attr ref android.R.styleable#Switch_thumb
513      */
getThumbDrawable()514     public Drawable getThumbDrawable() {
515         return mThumbDrawable;
516     }
517 
518     /**
519      * Specifies whether the track should be split by the thumb. When true,
520      * the thumb's optical bounds will be clipped out of the track drawable,
521      * then the thumb will be drawn into the resulting gap.
522      *
523      * @param splitTrack Whether the track should be split by the thumb
524      *
525      * @attr ref android.R.styleable#Switch_splitTrack
526      */
setSplitTrack(boolean splitTrack)527     public void setSplitTrack(boolean splitTrack) {
528         mSplitTrack = splitTrack;
529         invalidate();
530     }
531 
532     /**
533      * Returns whether the track should be split by the thumb.
534      *
535      * @attr ref android.R.styleable#Switch_splitTrack
536      */
getSplitTrack()537     public boolean getSplitTrack() {
538         return mSplitTrack;
539     }
540 
541     /**
542      * Returns the text displayed when the button is in the checked state.
543      *
544      * @attr ref android.R.styleable#Switch_textOn
545      */
getTextOn()546     public CharSequence getTextOn() {
547         return mTextOn;
548     }
549 
550     /**
551      * Sets the text displayed when the button is in the checked state.
552      *
553      * @attr ref android.R.styleable#Switch_textOn
554      */
setTextOn(CharSequence textOn)555     public void setTextOn(CharSequence textOn) {
556         mTextOn = textOn;
557         requestLayout();
558     }
559 
560     /**
561      * Returns the text displayed when the button is not in the checked state.
562      *
563      * @attr ref android.R.styleable#Switch_textOff
564      */
getTextOff()565     public CharSequence getTextOff() {
566         return mTextOff;
567     }
568 
569     /**
570      * Sets the text displayed when the button is not in the checked state.
571      *
572      * @attr ref android.R.styleable#Switch_textOff
573      */
setTextOff(CharSequence textOff)574     public void setTextOff(CharSequence textOff) {
575         mTextOff = textOff;
576         requestLayout();
577     }
578 
579     /**
580      * Sets whether the on/off text should be displayed.
581      *
582      * @param showText {@code true} to display on/off text
583      * @attr ref android.R.styleable#Switch_showText
584      */
setShowText(boolean showText)585     public void setShowText(boolean showText) {
586         if (mShowText != showText) {
587             mShowText = showText;
588             requestLayout();
589         }
590     }
591 
592     /**
593      * @return whether the on/off text should be displayed
594      * @attr ref android.R.styleable#Switch_showText
595      */
getShowText()596     public boolean getShowText() {
597         return mShowText;
598     }
599 
600     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)601     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
602         if (mShowText) {
603             if (mOnLayout == null) {
604                 mOnLayout = makeLayout(mTextOn);
605             }
606 
607             if (mOffLayout == null) {
608                 mOffLayout = makeLayout(mTextOff);
609             }
610         }
611 
612         final Rect padding = mTempRect;
613         final int thumbWidth;
614         final int thumbHeight;
615         if (mThumbDrawable != null) {
616             // Cached thumb width does not include padding.
617             mThumbDrawable.getPadding(padding);
618             thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
619             thumbHeight = mThumbDrawable.getIntrinsicHeight();
620         } else {
621             thumbWidth = 0;
622             thumbHeight = 0;
623         }
624 
625         final int maxTextWidth;
626         if (mShowText) {
627             maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
628                     + mThumbTextPadding * 2;
629         } else {
630             maxTextWidth = 0;
631         }
632 
633         mThumbWidth = Math.max(maxTextWidth, thumbWidth);
634 
635         final int trackHeight;
636         if (mTrackDrawable != null) {
637             mTrackDrawable.getPadding(padding);
638             trackHeight = mTrackDrawable.getIntrinsicHeight();
639         } else {
640             padding.setEmpty();
641             trackHeight = 0;
642         }
643 
644         // Adjust left and right padding to ensure there's enough room for the
645         // thumb's padding (when present).
646         int paddingLeft = padding.left;
647         int paddingRight = padding.right;
648         if (mThumbDrawable != null) {
649             final Insets inset = mThumbDrawable.getOpticalInsets();
650             paddingLeft = Math.max(paddingLeft, inset.left);
651             paddingRight = Math.max(paddingRight, inset.right);
652         }
653 
654         final int switchWidth = Math.max(mSwitchMinWidth,
655                 2 * mThumbWidth + paddingLeft + paddingRight);
656         final int switchHeight = Math.max(trackHeight, thumbHeight);
657         mSwitchWidth = switchWidth;
658         mSwitchHeight = switchHeight;
659 
660         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
661 
662         final int measuredHeight = getMeasuredHeight();
663         if (measuredHeight < switchHeight) {
664             setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
665         }
666     }
667 
668     @Override
onPopulateAccessibilityEvent(AccessibilityEvent event)669     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
670         super.onPopulateAccessibilityEvent(event);
671 
672         final CharSequence text = isChecked() ? mTextOn : mTextOff;
673         if (text != null) {
674             event.getText().add(text);
675         }
676     }
677 
makeLayout(CharSequence text)678     private Layout makeLayout(CharSequence text) {
679         final CharSequence transformed = (mSwitchTransformationMethod != null)
680                     ? mSwitchTransformationMethod.getTransformation(text, this)
681                     : text;
682 
683         return new StaticLayout(transformed, mTextPaint,
684                 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
685                 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
686     }
687 
688     /**
689      * @return true if (x, y) is within the target area of the switch thumb
690      */
hitThumb(float x, float y)691     private boolean hitThumb(float x, float y) {
692         if (mThumbDrawable == null) {
693             return false;
694         }
695 
696         // Relies on mTempRect, MUST be called first!
697         final int thumbOffset = getThumbOffset();
698 
699         mThumbDrawable.getPadding(mTempRect);
700         final int thumbTop = mSwitchTop - mTouchSlop;
701         final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
702         final int thumbRight = thumbLeft + mThumbWidth +
703                 mTempRect.left + mTempRect.right + mTouchSlop;
704         final int thumbBottom = mSwitchBottom + mTouchSlop;
705         return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
706     }
707 
708     @Override
onTouchEvent(MotionEvent ev)709     public boolean onTouchEvent(MotionEvent ev) {
710         mVelocityTracker.addMovement(ev);
711         final int action = ev.getActionMasked();
712         switch (action) {
713             case MotionEvent.ACTION_DOWN: {
714                 final float x = ev.getX();
715                 final float y = ev.getY();
716                 if (isEnabled() && hitThumb(x, y)) {
717                     mTouchMode = TOUCH_MODE_DOWN;
718                     mTouchX = x;
719                     mTouchY = y;
720                 }
721                 break;
722             }
723 
724             case MotionEvent.ACTION_MOVE: {
725                 switch (mTouchMode) {
726                     case TOUCH_MODE_IDLE:
727                         // Didn't target the thumb, treat normally.
728                         break;
729 
730                     case TOUCH_MODE_DOWN: {
731                         final float x = ev.getX();
732                         final float y = ev.getY();
733                         if (Math.abs(x - mTouchX) > mTouchSlop ||
734                                 Math.abs(y - mTouchY) > mTouchSlop) {
735                             mTouchMode = TOUCH_MODE_DRAGGING;
736                             getParent().requestDisallowInterceptTouchEvent(true);
737                             mTouchX = x;
738                             mTouchY = y;
739                             return true;
740                         }
741                         break;
742                     }
743 
744                     case TOUCH_MODE_DRAGGING: {
745                         final float x = ev.getX();
746                         final int thumbScrollRange = getThumbScrollRange();
747                         final float thumbScrollOffset = x - mTouchX;
748                         float dPos;
749                         if (thumbScrollRange != 0) {
750                             dPos = thumbScrollOffset / thumbScrollRange;
751                         } else {
752                             // If the thumb scroll range is empty, just use the
753                             // movement direction to snap on or off.
754                             dPos = thumbScrollOffset > 0 ? 1 : -1;
755                         }
756                         if (isLayoutRtl()) {
757                             dPos = -dPos;
758                         }
759                         final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
760                         if (newPos != mThumbPosition) {
761                             mTouchX = x;
762                             setThumbPosition(newPos);
763                         }
764                         return true;
765                     }
766                 }
767                 break;
768             }
769 
770             case MotionEvent.ACTION_UP:
771             case MotionEvent.ACTION_CANCEL: {
772                 if (mTouchMode == TOUCH_MODE_DRAGGING) {
773                     stopDrag(ev);
774                     // Allow super class to handle pressed state, etc.
775                     super.onTouchEvent(ev);
776                     return true;
777                 }
778                 mTouchMode = TOUCH_MODE_IDLE;
779                 mVelocityTracker.clear();
780                 break;
781             }
782         }
783 
784         return super.onTouchEvent(ev);
785     }
786 
cancelSuperTouch(MotionEvent ev)787     private void cancelSuperTouch(MotionEvent ev) {
788         MotionEvent cancel = MotionEvent.obtain(ev);
789         cancel.setAction(MotionEvent.ACTION_CANCEL);
790         super.onTouchEvent(cancel);
791         cancel.recycle();
792     }
793 
794     /**
795      * Called from onTouchEvent to end a drag operation.
796      *
797      * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
798      */
stopDrag(MotionEvent ev)799     private void stopDrag(MotionEvent ev) {
800         mTouchMode = TOUCH_MODE_IDLE;
801 
802         // Commit the change if the event is up and not canceled and the switch
803         // has not been disabled during the drag.
804         final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
805         final boolean oldState = isChecked();
806         final boolean newState;
807         if (commitChange) {
808             mVelocityTracker.computeCurrentVelocity(1000);
809             final float xvel = mVelocityTracker.getXVelocity();
810             if (Math.abs(xvel) > mMinFlingVelocity) {
811                 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
812             } else {
813                 newState = getTargetCheckedState();
814             }
815         } else {
816             newState = oldState;
817         }
818 
819         if (newState != oldState) {
820             playSoundEffect(SoundEffectConstants.CLICK);
821             setChecked(newState);
822         }
823 
824         cancelSuperTouch(ev);
825     }
826 
animateThumbToCheckedState(boolean newCheckedState)827     private void animateThumbToCheckedState(boolean newCheckedState) {
828         final float targetPosition = newCheckedState ? 1 : 0;
829         mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
830         mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
831         mPositionAnimator.setAutoCancel(true);
832         mPositionAnimator.start();
833     }
834 
cancelPositionAnimator()835     private void cancelPositionAnimator() {
836         if (mPositionAnimator != null) {
837             mPositionAnimator.cancel();
838         }
839     }
840 
getTargetCheckedState()841     private boolean getTargetCheckedState() {
842         return mThumbPosition > 0.5f;
843     }
844 
845     /**
846      * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
847      *
848      * @param position new position between [0,1]
849      */
setThumbPosition(float position)850     private void setThumbPosition(float position) {
851         mThumbPosition = position;
852         invalidate();
853     }
854 
855     @Override
toggle()856     public void toggle() {
857         setChecked(!isChecked());
858     }
859 
860     @Override
setChecked(boolean checked)861     public void setChecked(boolean checked) {
862         super.setChecked(checked);
863 
864         // Calling the super method may result in setChecked() getting called
865         // recursively with a different value, so load the REAL value...
866         checked = isChecked();
867 
868         if (isAttachedToWindow() && isLaidOut()) {
869             animateThumbToCheckedState(checked);
870         } else {
871             // Immediately move the thumb to the new position.
872             cancelPositionAnimator();
873             setThumbPosition(checked ? 1 : 0);
874         }
875     }
876 
877     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)878     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
879         super.onLayout(changed, left, top, right, bottom);
880 
881         int opticalInsetLeft = 0;
882         int opticalInsetRight = 0;
883         if (mThumbDrawable != null) {
884             final Rect trackPadding = mTempRect;
885             if (mTrackDrawable != null) {
886                 mTrackDrawable.getPadding(trackPadding);
887             } else {
888                 trackPadding.setEmpty();
889             }
890 
891             final Insets insets = mThumbDrawable.getOpticalInsets();
892             opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
893             opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
894         }
895 
896         final int switchRight;
897         final int switchLeft;
898         if (isLayoutRtl()) {
899             switchLeft = getPaddingLeft() + opticalInsetLeft;
900             switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
901         } else {
902             switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
903             switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
904         }
905 
906         final int switchTop;
907         final int switchBottom;
908         switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
909             default:
910             case Gravity.TOP:
911                 switchTop = getPaddingTop();
912                 switchBottom = switchTop + mSwitchHeight;
913                 break;
914 
915             case Gravity.CENTER_VERTICAL:
916                 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
917                         mSwitchHeight / 2;
918                 switchBottom = switchTop + mSwitchHeight;
919                 break;
920 
921             case Gravity.BOTTOM:
922                 switchBottom = getHeight() - getPaddingBottom();
923                 switchTop = switchBottom - mSwitchHeight;
924                 break;
925         }
926 
927         mSwitchLeft = switchLeft;
928         mSwitchTop = switchTop;
929         mSwitchBottom = switchBottom;
930         mSwitchRight = switchRight;
931     }
932 
933     @Override
draw(Canvas c)934     public void draw(Canvas c) {
935         final Rect padding = mTempRect;
936         final int switchLeft = mSwitchLeft;
937         final int switchTop = mSwitchTop;
938         final int switchRight = mSwitchRight;
939         final int switchBottom = mSwitchBottom;
940 
941         int thumbInitialLeft = switchLeft + getThumbOffset();
942 
943         final Insets thumbInsets;
944         if (mThumbDrawable != null) {
945             thumbInsets = mThumbDrawable.getOpticalInsets();
946         } else {
947             thumbInsets = Insets.NONE;
948         }
949 
950         // Layout the track.
951         if (mTrackDrawable != null) {
952             mTrackDrawable.getPadding(padding);
953 
954             // Adjust thumb position for track padding.
955             thumbInitialLeft += padding.left;
956 
957             // If necessary, offset by the optical insets of the thumb asset.
958             int trackLeft = switchLeft;
959             int trackTop = switchTop;
960             int trackRight = switchRight;
961             int trackBottom = switchBottom;
962             if (thumbInsets != Insets.NONE) {
963                 if (thumbInsets.left > padding.left) {
964                     trackLeft += thumbInsets.left - padding.left;
965                 }
966                 if (thumbInsets.top > padding.top) {
967                     trackTop += thumbInsets.top - padding.top;
968                 }
969                 if (thumbInsets.right > padding.right) {
970                     trackRight -= thumbInsets.right - padding.right;
971                 }
972                 if (thumbInsets.bottom > padding.bottom) {
973                     trackBottom -= thumbInsets.bottom - padding.bottom;
974                 }
975             }
976             mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
977         }
978 
979         // Layout the thumb.
980         if (mThumbDrawable != null) {
981             mThumbDrawable.getPadding(padding);
982 
983             final int thumbLeft = thumbInitialLeft - padding.left;
984             final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
985             mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
986 
987             final Drawable background = getBackground();
988             if (background != null) {
989                 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
990             }
991         }
992 
993         // Draw the background.
994         super.draw(c);
995     }
996 
997     @Override
onDraw(Canvas canvas)998     protected void onDraw(Canvas canvas) {
999         super.onDraw(canvas);
1000 
1001         final Rect padding = mTempRect;
1002         final Drawable trackDrawable = mTrackDrawable;
1003         if (trackDrawable != null) {
1004             trackDrawable.getPadding(padding);
1005         } else {
1006             padding.setEmpty();
1007         }
1008 
1009         final int switchTop = mSwitchTop;
1010         final int switchBottom = mSwitchBottom;
1011         final int switchInnerTop = switchTop + padding.top;
1012         final int switchInnerBottom = switchBottom - padding.bottom;
1013 
1014         final Drawable thumbDrawable = mThumbDrawable;
1015         if (trackDrawable != null) {
1016             if (mSplitTrack && thumbDrawable != null) {
1017                 final Insets insets = thumbDrawable.getOpticalInsets();
1018                 thumbDrawable.copyBounds(padding);
1019                 padding.left += insets.left;
1020                 padding.right -= insets.right;
1021 
1022                 final int saveCount = canvas.save();
1023                 canvas.clipRect(padding, Op.DIFFERENCE);
1024                 trackDrawable.draw(canvas);
1025                 canvas.restoreToCount(saveCount);
1026             } else {
1027                 trackDrawable.draw(canvas);
1028             }
1029         }
1030 
1031         final int saveCount = canvas.save();
1032 
1033         if (thumbDrawable != null) {
1034             thumbDrawable.draw(canvas);
1035         }
1036 
1037         final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
1038         if (switchText != null) {
1039             final int drawableState[] = getDrawableState();
1040             if (mTextColors != null) {
1041                 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1042             }
1043             mTextPaint.drawableState = drawableState;
1044 
1045             final int cX;
1046             if (thumbDrawable != null) {
1047                 final Rect bounds = thumbDrawable.getBounds();
1048                 cX = bounds.left + bounds.right;
1049             } else {
1050                 cX = getWidth();
1051             }
1052 
1053             final int left = cX / 2 - switchText.getWidth() / 2;
1054             final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1055             canvas.translate(left, top);
1056             switchText.draw(canvas);
1057         }
1058 
1059         canvas.restoreToCount(saveCount);
1060     }
1061 
1062     @Override
getCompoundPaddingLeft()1063     public int getCompoundPaddingLeft() {
1064         if (!isLayoutRtl()) {
1065             return super.getCompoundPaddingLeft();
1066         }
1067         int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1068         if (!TextUtils.isEmpty(getText())) {
1069             padding += mSwitchPadding;
1070         }
1071         return padding;
1072     }
1073 
1074     @Override
getCompoundPaddingRight()1075     public int getCompoundPaddingRight() {
1076         if (isLayoutRtl()) {
1077             return super.getCompoundPaddingRight();
1078         }
1079         int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1080         if (!TextUtils.isEmpty(getText())) {
1081             padding += mSwitchPadding;
1082         }
1083         return padding;
1084     }
1085 
1086     /**
1087      * Translates thumb position to offset according to current RTL setting and
1088      * thumb scroll range. Accounts for both track and thumb padding.
1089      *
1090      * @return thumb offset
1091      */
getThumbOffset()1092     private int getThumbOffset() {
1093         final float thumbPosition;
1094         if (isLayoutRtl()) {
1095             thumbPosition = 1 - mThumbPosition;
1096         } else {
1097             thumbPosition = mThumbPosition;
1098         }
1099         return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1100     }
1101 
getThumbScrollRange()1102     private int getThumbScrollRange() {
1103         if (mTrackDrawable != null) {
1104             final Rect padding = mTempRect;
1105             mTrackDrawable.getPadding(padding);
1106 
1107             final Insets insets;
1108             if (mThumbDrawable != null) {
1109                 insets = mThumbDrawable.getOpticalInsets();
1110             } else {
1111                 insets = Insets.NONE;
1112             }
1113 
1114             return mSwitchWidth - mThumbWidth - padding.left - padding.right
1115                     - insets.left - insets.right;
1116         } else {
1117             return 0;
1118         }
1119     }
1120 
1121     @Override
onCreateDrawableState(int extraSpace)1122     protected int[] onCreateDrawableState(int extraSpace) {
1123         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1124         if (isChecked()) {
1125             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1126         }
1127         return drawableState;
1128     }
1129 
1130     @Override
drawableStateChanged()1131     protected void drawableStateChanged() {
1132         super.drawableStateChanged();
1133 
1134         final int[] myDrawableState = getDrawableState();
1135 
1136         if (mThumbDrawable != null) {
1137             mThumbDrawable.setState(myDrawableState);
1138         }
1139 
1140         if (mTrackDrawable != null) {
1141             mTrackDrawable.setState(myDrawableState);
1142         }
1143 
1144         invalidate();
1145     }
1146 
1147     @Override
drawableHotspotChanged(float x, float y)1148     public void drawableHotspotChanged(float x, float y) {
1149         super.drawableHotspotChanged(x, y);
1150 
1151         if (mThumbDrawable != null) {
1152             mThumbDrawable.setHotspot(x, y);
1153         }
1154 
1155         if (mTrackDrawable != null) {
1156             mTrackDrawable.setHotspot(x, y);
1157         }
1158     }
1159 
1160     @Override
verifyDrawable(Drawable who)1161     protected boolean verifyDrawable(Drawable who) {
1162         return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1163     }
1164 
1165     @Override
jumpDrawablesToCurrentState()1166     public void jumpDrawablesToCurrentState() {
1167         super.jumpDrawablesToCurrentState();
1168 
1169         if (mThumbDrawable != null) {
1170             mThumbDrawable.jumpToCurrentState();
1171         }
1172 
1173         if (mTrackDrawable != null) {
1174             mTrackDrawable.jumpToCurrentState();
1175         }
1176 
1177         if (mPositionAnimator != null && mPositionAnimator.isRunning()) {
1178             mPositionAnimator.end();
1179             mPositionAnimator = null;
1180         }
1181     }
1182 
1183     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)1184     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1185         super.onInitializeAccessibilityEvent(event);
1186         event.setClassName(Switch.class.getName());
1187     }
1188 
1189     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1190     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1191         super.onInitializeAccessibilityNodeInfo(info);
1192         info.setClassName(Switch.class.getName());
1193         CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1194         if (!TextUtils.isEmpty(switchText)) {
1195             CharSequence oldText = info.getText();
1196             if (TextUtils.isEmpty(oldText)) {
1197                 info.setText(switchText);
1198             } else {
1199                 StringBuilder newText = new StringBuilder();
1200                 newText.append(oldText).append(' ').append(switchText);
1201                 info.setText(newText);
1202             }
1203         }
1204     }
1205 
1206     private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1207         @Override
1208         public Float get(Switch object) {
1209             return object.mThumbPosition;
1210         }
1211 
1212         @Override
1213         public void setValue(Switch object, float value) {
1214             object.setThumbPosition(value);
1215         }
1216     };
1217 }
1218