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