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