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