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