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