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