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