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