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