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