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