blob: f42959ffe28f74bf24c88ee4d60fa6b47c0a7aa8 [file] [log] [blame]
Adam Powell12190b32010-11-28 19:07:53 -08001/*
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
17package android.widget;
18
Alan Viverettecc2688d2013-09-17 17:00:12 -070019import android.animation.ObjectAnimator;
Tor Norbye7b9c9122013-05-30 16:48:33 -070020import android.annotation.DrawableRes;
Alan Viverettee7eee642015-01-29 14:27:49 -080021import android.annotation.Nullable;
Tor Norbye7b9c9122013-05-30 16:48:33 -070022import android.annotation.StyleRes;
Adam Powell12190b32010-11-28 19:07:53 -080023import android.content.Context;
24import android.content.res.ColorStateList;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
Alan Viverette661e6362014-05-12 10:55:37 -070028import android.graphics.Insets;
Chris Craik6a49dde2015-05-12 10:28:14 -070029import android.graphics.Paint;
Alan Viverettee7eee642015-01-29 14:27:49 -080030import android.graphics.PorterDuff;
Adam Powell12190b32010-11-28 19:07:53 -080031import android.graphics.Rect;
32import android.graphics.Typeface;
Alan Viverette661e6362014-05-12 10:55:37 -070033import android.graphics.Region.Op;
Adam Powell12190b32010-11-28 19:07:53 -080034import android.graphics.drawable.Drawable;
35import android.text.Layout;
36import android.text.StaticLayout;
37import android.text.TextPaint;
38import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040039import android.text.method.AllCapsTransformationMethod;
40import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080041import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070042import android.util.FloatProperty;
43import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080044import android.view.Gravity;
45import android.view.MotionEvent;
Alan Viveretted4e77902014-10-27 17:50:51 -070046import android.view.SoundEffectConstants;
Adam Powell12190b32010-11-28 19:07:53 -080047import android.view.VelocityTracker;
Dianne Hackborn49b043f2015-05-07 14:21:38 -070048import android.view.ViewStructure;
Adam Powell12190b32010-11-28 19:07:53 -080049import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070050import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080051import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080052
Adam Powellbe0a4532010-11-29 17:47:48 -080053import com.android.internal.R;
54
Adam Powell12190b32010-11-28 19:07:53 -080055/**
56 * A Switch is a two-state toggle switch widget that can select between two
57 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070058 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
59 * property controls the text displayed in the label for the switch, whereas the
60 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
61 * controls the text on the thumb. Similarly, the
62 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
63 * setTypeface() methods control the typeface and style of label text, whereas the
64 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
65 * the related seSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080066 *
Scott Main4c359b72012-07-24 15:51:27 -070067 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
68 * guide.</p>
69 *
70 * @attr ref android.R.styleable#Switch_textOn
71 * @attr ref android.R.styleable#Switch_textOff
72 * @attr ref android.R.styleable#Switch_switchMinWidth
73 * @attr ref android.R.styleable#Switch_switchPadding
74 * @attr ref android.R.styleable#Switch_switchTextAppearance
75 * @attr ref android.R.styleable#Switch_thumb
76 * @attr ref android.R.styleable#Switch_thumbTextPadding
77 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080078 */
79public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070080 private static final int THUMB_ANIMATION_DURATION = 250;
81
Adam Powell12190b32010-11-28 19:07:53 -080082 private static final int TOUCH_MODE_IDLE = 0;
83 private static final int TOUCH_MODE_DOWN = 1;
84 private static final int TOUCH_MODE_DRAGGING = 2;
85
86 // Enum for the "typeface" XML parameter.
87 private static final int SANS = 1;
88 private static final int SERIF = 2;
89 private static final int MONOSPACE = 3;
90
91 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080092 private ColorStateList mThumbTintList = null;
93 private PorterDuff.Mode mThumbTintMode = null;
94 private boolean mHasThumbTint = false;
95 private boolean mHasThumbTintMode = false;
96
Adam Powell12190b32010-11-28 19:07:53 -080097 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080098 private ColorStateList mTrackTintList = null;
99 private PorterDuff.Mode mTrackTintMode = null;
100 private boolean mHasTrackTint = false;
101 private boolean mHasTrackTintMode = false;
102
Adam Powell12190b32010-11-28 19:07:53 -0800103 private int mThumbTextPadding;
104 private int mSwitchMinWidth;
105 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700106 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800107 private CharSequence mTextOn;
108 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700109 private boolean mShowText;
Adam Powell12190b32010-11-28 19:07:53 -0800110
111 private int mTouchMode;
112 private int mTouchSlop;
113 private float mTouchX;
114 private float mTouchY;
115 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
116 private int mMinFlingVelocity;
117
118 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700119
Alan Viverette0c0dde72014-07-30 13:29:39 -0700120 /**
121 * Width required to draw the switch track and thumb. Includes padding and
122 * optical bounds for both the track and thumb.
123 */
124 private int mSwitchWidth;
125
126 /**
127 * Height required to draw the switch track and thumb. Includes padding and
128 * optical bounds for both the track and thumb.
129 */
130 private int mSwitchHeight;
131
132 /**
133 * Width of the thumb's content region. Does not include padding or
134 * optical bounds.
135 */
136 private int mThumbWidth;
137
138 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800139 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700140
141 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800142 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700143
144 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800145 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700146
147 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800148 private int mSwitchBottom;
149
150 private TextPaint mTextPaint;
151 private ColorStateList mTextColors;
152 private Layout mOnLayout;
153 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400154 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700155 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800156
Adam Powellbe0a4532010-11-29 17:47:48 -0800157 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800158 private final Rect mTempRect = new Rect();
159
160 private static final int[] CHECKED_STATE_SET = {
161 R.attr.state_checked
162 };
163
164 /**
165 * Construct a new Switch with default styling.
166 *
167 * @param context The Context that will determine this widget's theming.
168 */
169 public Switch(Context context) {
170 this(context, null);
171 }
172
173 /**
174 * Construct a new Switch with default styling, overriding specific style
175 * attributes as requested.
176 *
177 * @param context The Context that will determine this widget's theming.
178 * @param attrs Specification of attributes that should deviate from default styling.
179 */
180 public Switch(Context context, AttributeSet attrs) {
181 this(context, attrs, com.android.internal.R.attr.switchStyle);
182 }
183
184 /**
185 * Construct a new Switch with a default style determined by the given theme attribute,
186 * overriding specific style attributes as 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 default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700190 * @param defStyleAttr An attribute in the current theme that contains a
191 * reference to a style resource that supplies default values for
192 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800193 */
Alan Viverette617feb92013-09-09 18:09:13 -0700194 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
195 this(context, attrs, defStyleAttr, 0);
196 }
197
198
199 /**
200 * Construct a new Switch with a default style determined by the given theme
201 * attribute or style resource, overriding specific style attributes as
202 * requested.
203 *
204 * @param context The Context that will determine this widget's theming.
205 * @param attrs Specification of attributes that should deviate from the
206 * default styling.
207 * @param defStyleAttr An attribute in the current theme that contains a
208 * reference to a style resource that supplies default values for
209 * the view. Can be 0 to not look for defaults.
210 * @param defStyleRes A resource identifier of a style resource that
211 * supplies default values for the view, used only if
212 * defStyleAttr is 0 or can not be found in the theme. Can be 0
213 * to not look for defaults.
214 */
215 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
216 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800217
Chris Craik6a49dde2015-05-12 10:28:14 -0700218 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700219
220 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800221 mTextPaint.density = res.getDisplayMetrics().density;
222 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
223
Alan Viverette617feb92013-09-09 18:09:13 -0700224 final TypedArray a = context.obtainStyledAttributes(
225 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700226 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700227 if (mThumbDrawable != null) {
228 mThumbDrawable.setCallback(this);
229 }
Chet Haase150176d2011-08-26 09:54:06 -0700230 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700231 if (mTrackDrawable != null) {
232 mTrackDrawable.setCallback(this);
233 }
Adam Powell12190b32010-11-28 19:07:53 -0800234 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
235 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700236 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800237 mThumbTextPadding = a.getDimensionPixelSize(
238 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
239 mSwitchMinWidth = a.getDimensionPixelSize(
240 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
241 mSwitchPadding = a.getDimensionPixelSize(
242 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700243 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800244
Alan Viverette661e6362014-05-12 10:55:37 -0700245 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800246 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
247 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700248 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800249 }
250 a.recycle();
251
Alan Viverette661e6362014-05-12 10:55:37 -0700252 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800253 mTouchSlop = config.getScaledTouchSlop();
254 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
255
256 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700257 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800258 setChecked(isChecked());
259 }
260
261 /**
262 * Sets the switch text color, size, style, hint color, and highlight color
263 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800264 *
265 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800266 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700267 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800268 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700269 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800270 com.android.internal.R.styleable.TextAppearance);
271
272 ColorStateList colors;
273 int ts;
274
275 colors = appearance.getColorStateList(com.android.internal.R.styleable.
276 TextAppearance_textColor);
277 if (colors != null) {
278 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700279 } else {
280 // If no color set in TextAppearance, default to the view's textColor
281 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800282 }
283
284 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
285 TextAppearance_textSize, 0);
286 if (ts != 0) {
287 if (ts != mTextPaint.getTextSize()) {
288 mTextPaint.setTextSize(ts);
289 requestLayout();
290 }
291 }
292
293 int typefaceIndex, styleIndex;
294
295 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
296 TextAppearance_typeface, -1);
297 styleIndex = appearance.getInt(com.android.internal.R.styleable.
298 TextAppearance_textStyle, -1);
299
300 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
301
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400302 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
303 TextAppearance_textAllCaps, false);
304 if (allCaps) {
305 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
306 mSwitchTransformationMethod.setLengthChangesAllowed(true);
307 } else {
308 mSwitchTransformationMethod = null;
309 }
310
Adam Powell12190b32010-11-28 19:07:53 -0800311 appearance.recycle();
312 }
313
314 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
315 Typeface tf = null;
316 switch (typefaceIndex) {
317 case SANS:
318 tf = Typeface.SANS_SERIF;
319 break;
320
321 case SERIF:
322 tf = Typeface.SERIF;
323 break;
324
325 case MONOSPACE:
326 tf = Typeface.MONOSPACE;
327 break;
328 }
329
330 setSwitchTypeface(tf, styleIndex);
331 }
332
333 /**
334 * Sets the typeface and style in which the text should be displayed on the
335 * switch, and turns on the fake bold and italic bits in the Paint if the
336 * Typeface that you provided does not have all the bits in the
337 * style that you specified.
338 */
339 public void setSwitchTypeface(Typeface tf, int style) {
340 if (style > 0) {
341 if (tf == null) {
342 tf = Typeface.defaultFromStyle(style);
343 } else {
344 tf = Typeface.create(tf, style);
345 }
346
347 setSwitchTypeface(tf);
348 // now compute what (if any) algorithmic styling is needed
349 int typefaceStyle = tf != null ? tf.getStyle() : 0;
350 int need = style & ~typefaceStyle;
351 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
352 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
353 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700354 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800355 mTextPaint.setTextSkewX(0);
356 setSwitchTypeface(tf);
357 }
358 }
359
360 /**
Chet Haase150176d2011-08-26 09:54:06 -0700361 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800362 * Note that not all Typeface families actually have bold and italic
363 * variants, so you may need to use
364 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
365 * that you actually want.
366 *
367 * @attr ref android.R.styleable#TextView_typeface
368 * @attr ref android.R.styleable#TextView_textStyle
369 */
370 public void setSwitchTypeface(Typeface tf) {
371 if (mTextPaint.getTypeface() != tf) {
372 mTextPaint.setTypeface(tf);
373
374 requestLayout();
375 invalidate();
376 }
377 }
378
379 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800380 * Set the amount of horizontal padding between the switch and the associated text.
381 *
382 * @param pixels Amount of padding in pixels
383 *
384 * @attr ref android.R.styleable#Switch_switchPadding
385 */
386 public void setSwitchPadding(int pixels) {
387 mSwitchPadding = pixels;
388 requestLayout();
389 }
390
391 /**
392 * Get the amount of horizontal padding between the switch and the associated text.
393 *
394 * @return Amount of padding in pixels
395 *
396 * @attr ref android.R.styleable#Switch_switchPadding
397 */
398 public int getSwitchPadding() {
399 return mSwitchPadding;
400 }
401
402 /**
403 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
404 * of this value and its measured width as determined by the switch drawables and text used.
405 *
406 * @param pixels Minimum width of the switch in pixels
407 *
408 * @attr ref android.R.styleable#Switch_switchMinWidth
409 */
410 public void setSwitchMinWidth(int pixels) {
411 mSwitchMinWidth = pixels;
412 requestLayout();
413 }
414
415 /**
416 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
417 * of this value and its measured width as determined by the switch drawables and text used.
418 *
419 * @return Minimum width of the switch in pixels
420 *
421 * @attr ref android.R.styleable#Switch_switchMinWidth
422 */
423 public int getSwitchMinWidth() {
424 return mSwitchMinWidth;
425 }
426
427 /**
428 * Set the horizontal padding around the text drawn on the switch itself.
429 *
430 * @param pixels Horizontal padding for switch thumb text in pixels
431 *
432 * @attr ref android.R.styleable#Switch_thumbTextPadding
433 */
434 public void setThumbTextPadding(int pixels) {
435 mThumbTextPadding = pixels;
436 requestLayout();
437 }
438
439 /**
440 * Get the horizontal padding around the text drawn on the switch itself.
441 *
442 * @return Horizontal padding for switch thumb text in pixels
443 *
444 * @attr ref android.R.styleable#Switch_thumbTextPadding
445 */
446 public int getThumbTextPadding() {
447 return mThumbTextPadding;
448 }
449
450 /**
451 * Set the drawable used for the track that the switch slides within.
452 *
453 * @param track Track drawable
454 *
455 * @attr ref android.R.styleable#Switch_track
456 */
457 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700458 if (mTrackDrawable != null) {
459 mTrackDrawable.setCallback(null);
460 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800461 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700462 if (track != null) {
463 track.setCallback(this);
464 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800465 requestLayout();
466 }
467
468 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800469 * Set the drawable used for the track that the switch slides within.
470 *
Adam Powelldca510e2012-03-08 20:06:39 -0800471 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800472 *
473 * @attr ref android.R.styleable#Switch_track
474 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700475 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800476 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800477 }
478
479 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800480 * Get the drawable used for the track that the switch slides within.
481 *
482 * @return Track drawable
483 *
484 * @attr ref android.R.styleable#Switch_track
485 */
486 public Drawable getTrackDrawable() {
487 return mTrackDrawable;
488 }
489
490 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800491 * Applies a tint to the track drawable. Does not modify the current
492 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
493 * <p>
494 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
495 * automatically mutate the drawable and apply the specified tint and tint
496 * mode using {@link Drawable#setTintList(ColorStateList)}.
497 *
498 * @param tint the tint to apply, may be {@code null} to clear tint
499 *
500 * @attr ref android.R.styleable#Switch_trackTint
501 * @see #getTrackTintList()
502 * @see Drawable#setTintList(ColorStateList)
503 */
504 public void setTrackTintList(@Nullable ColorStateList tint) {
505 mTrackTintList = tint;
506 mHasTrackTint = true;
507
508 applyTrackTint();
509 }
510
511 /**
512 * @return the tint applied to the track drawable
513 * @attr ref android.R.styleable#Switch_trackTint
514 * @see #setTrackTintList(ColorStateList)
515 */
516 @Nullable
517 public ColorStateList getTrackTintList() {
518 return mTrackTintList;
519 }
520
521 /**
522 * Specifies the blending mode used to apply the tint specified by
523 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
524 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
525 *
526 * @param tintMode the blending mode used to apply the tint, may be
527 * {@code null} to clear tint
528 * @attr ref android.R.styleable#Switch_trackTintMode
529 * @see #getTrackTintMode()
530 * @see Drawable#setTintMode(PorterDuff.Mode)
531 */
532 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
533 mTrackTintMode = tintMode;
534 mHasTrackTintMode = true;
535
536 applyTrackTint();
537 }
538
539 /**
540 * @return the blending mode used to apply the tint to the track
541 * drawable
542 * @attr ref android.R.styleable#Switch_trackTintMode
543 * @see #setTrackTintMode(PorterDuff.Mode)
544 */
545 @Nullable
546 public PorterDuff.Mode getTrackTintMode() {
547 return mTrackTintMode;
548 }
549
550 private void applyTrackTint() {
551 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
552 mTrackDrawable = mTrackDrawable.mutate();
553
554 if (mHasTrackTint) {
555 mTrackDrawable.setTintList(mTrackTintList);
556 }
557
558 if (mHasTrackTintMode) {
559 mTrackDrawable.setTintMode(mTrackTintMode);
560 }
561
562 // The drawable (or one of its children) may not have been
563 // stateful before applying the tint, so let's try again.
564 if (mTrackDrawable.isStateful()) {
565 mTrackDrawable.setState(getDrawableState());
566 }
567 }
568 }
569
570 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800571 * Set the drawable used for the switch "thumb" - the piece that the user
572 * can physically touch and drag along the track.
573 *
574 * @param thumb Thumb drawable
575 *
576 * @attr ref android.R.styleable#Switch_thumb
577 */
578 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700579 if (mThumbDrawable != null) {
580 mThumbDrawable.setCallback(null);
581 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800582 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700583 if (thumb != null) {
584 thumb.setCallback(this);
585 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800586 requestLayout();
587 }
588
589 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800590 * Set the drawable used for the switch "thumb" - the piece that the user
591 * can physically touch and drag along the track.
592 *
Adam Powelldca510e2012-03-08 20:06:39 -0800593 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800594 *
595 * @attr ref android.R.styleable#Switch_thumb
596 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700597 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800598 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800599 }
600
601 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800602 * Get the drawable used for the switch "thumb" - the piece that the user
603 * can physically touch and drag along the track.
604 *
605 * @return Thumb drawable
606 *
607 * @attr ref android.R.styleable#Switch_thumb
608 */
609 public Drawable getThumbDrawable() {
610 return mThumbDrawable;
611 }
612
613 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800614 * Applies a tint to the thumb drawable. Does not modify the current
615 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
616 * <p>
617 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
618 * automatically mutate the drawable and apply the specified tint and tint
619 * mode using {@link Drawable#setTintList(ColorStateList)}.
620 *
621 * @param tint the tint to apply, may be {@code null} to clear tint
622 *
623 * @attr ref android.R.styleable#Switch_thumbTint
624 * @see #getThumbTintList()
625 * @see Drawable#setTintList(ColorStateList)
626 */
627 public void setThumbTintList(@Nullable ColorStateList tint) {
628 mThumbTintList = tint;
629 mHasThumbTint = true;
630
631 applyThumbTint();
632 }
633
634 /**
635 * @return the tint applied to the thumb drawable
636 * @attr ref android.R.styleable#Switch_thumbTint
637 * @see #setThumbTintList(ColorStateList)
638 */
639 @Nullable
640 public ColorStateList getThumbTintList() {
641 return mThumbTintList;
642 }
643
644 /**
645 * Specifies the blending mode used to apply the tint specified by
646 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
647 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
648 *
649 * @param tintMode the blending mode used to apply the tint, may be
650 * {@code null} to clear tint
651 * @attr ref android.R.styleable#Switch_thumbTintMode
652 * @see #getThumbTintMode()
653 * @see Drawable#setTintMode(PorterDuff.Mode)
654 */
655 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
656 mThumbTintMode = tintMode;
657 mHasThumbTintMode = true;
658
659 applyThumbTint();
660 }
661
662 /**
663 * @return the blending mode used to apply the tint to the thumb
664 * drawable
665 * @attr ref android.R.styleable#Switch_thumbTintMode
666 * @see #setThumbTintMode(PorterDuff.Mode)
667 */
668 @Nullable
669 public PorterDuff.Mode getThumbTintMode() {
670 return mThumbTintMode;
671 }
672
673 private void applyThumbTint() {
674 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
675 mThumbDrawable = mThumbDrawable.mutate();
676
677 if (mHasThumbTint) {
678 mThumbDrawable.setTintList(mThumbTintList);
679 }
680
681 if (mHasThumbTintMode) {
682 mThumbDrawable.setTintMode(mThumbTintMode);
683 }
684
685 // The drawable (or one of its children) may not have been
686 // stateful before applying the tint, so let's try again.
687 if (mThumbDrawable.isStateful()) {
688 mThumbDrawable.setState(getDrawableState());
689 }
690 }
691 }
692
693 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700694 * Specifies whether the track should be split by the thumb. When true,
695 * the thumb's optical bounds will be clipped out of the track drawable,
696 * then the thumb will be drawn into the resulting gap.
697 *
698 * @param splitTrack Whether the track should be split by the thumb
699 *
700 * @attr ref android.R.styleable#Switch_splitTrack
701 */
702 public void setSplitTrack(boolean splitTrack) {
703 mSplitTrack = splitTrack;
704 invalidate();
705 }
706
707 /**
708 * Returns whether the track should be split by the thumb.
709 *
710 * @attr ref android.R.styleable#Switch_splitTrack
711 */
712 public boolean getSplitTrack() {
713 return mSplitTrack;
714 }
715
716 /**
Chet Haase150176d2011-08-26 09:54:06 -0700717 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800718 *
719 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800720 */
721 public CharSequence getTextOn() {
722 return mTextOn;
723 }
724
725 /**
Chet Haase150176d2011-08-26 09:54:06 -0700726 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800727 *
728 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800729 */
730 public void setTextOn(CharSequence textOn) {
731 mTextOn = textOn;
732 requestLayout();
733 }
734
735 /**
Chet Haase150176d2011-08-26 09:54:06 -0700736 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800737 *
738 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800739 */
740 public CharSequence getTextOff() {
741 return mTextOff;
742 }
743
744 /**
Chet Haase150176d2011-08-26 09:54:06 -0700745 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800746 *
747 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800748 */
749 public void setTextOff(CharSequence textOff) {
750 mTextOff = textOff;
751 requestLayout();
752 }
753
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700754 /**
755 * Sets whether the on/off text should be displayed.
756 *
757 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700758 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700759 */
760 public void setShowText(boolean showText) {
761 if (mShowText != showText) {
762 mShowText = showText;
763 requestLayout();
764 }
765 }
766
767 /**
768 * @return whether the on/off text should be displayed
Alan Viverette0c0dde72014-07-30 13:29:39 -0700769 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700770 */
771 public boolean getShowText() {
772 return mShowText;
773 }
774
Adam Powell12190b32010-11-28 19:07:53 -0800775 @Override
776 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700777 if (mShowText) {
778 if (mOnLayout == null) {
779 mOnLayout = makeLayout(mTextOn);
780 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800781
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700782 if (mOffLayout == null) {
783 mOffLayout = makeLayout(mTextOff);
784 }
Adam Powell12190b32010-11-28 19:07:53 -0800785 }
786
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000787 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700788 final int thumbWidth;
789 final int thumbHeight;
790 if (mThumbDrawable != null) {
791 // Cached thumb width does not include padding.
792 mThumbDrawable.getPadding(padding);
793 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
794 thumbHeight = mThumbDrawable.getIntrinsicHeight();
795 } else {
796 thumbWidth = 0;
797 thumbHeight = 0;
798 }
799
800 final int maxTextWidth;
801 if (mShowText) {
802 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
803 + mThumbTextPadding * 2;
804 } else {
805 maxTextWidth = 0;
806 }
807
808 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
809
810 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700811 if (mTrackDrawable != null) {
812 mTrackDrawable.getPadding(padding);
813 trackHeight = mTrackDrawable.getIntrinsicHeight();
814 } else {
815 padding.setEmpty();
816 trackHeight = 0;
817 }
818
Alan Viverette0c0dde72014-07-30 13:29:39 -0700819 // Adjust left and right padding to ensure there's enough room for the
820 // thumb's padding (when present).
821 int paddingLeft = padding.left;
822 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700823 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700824 final Insets inset = mThumbDrawable.getOpticalInsets();
825 paddingLeft = Math.max(paddingLeft, inset.left);
826 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700827 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800828
Adam Powell12190b32010-11-28 19:07:53 -0800829 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700830 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700831 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800832 mSwitchWidth = switchWidth;
833 mSwitchHeight = switchHeight;
834
835 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700836
Adam Powell12190b32010-11-28 19:07:53 -0800837 final int measuredHeight = getMeasuredHeight();
838 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800839 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800840 }
841 }
842
Alan Viverettea54956a2015-01-07 16:05:02 -0800843 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700844 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800845 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
846 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700847
848 final CharSequence text = isChecked() ? mTextOn : mTextOff;
849 if (text != null) {
850 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700851 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700852 }
853
Adam Powell12190b32010-11-28 19:07:53 -0800854 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400855 final CharSequence transformed = (mSwitchTransformationMethod != null)
856 ? mSwitchTransformationMethod.getTransformation(text, this)
857 : text;
858
859 return new StaticLayout(transformed, mTextPaint,
860 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800861 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
862 }
863
864 /**
865 * @return true if (x, y) is within the target area of the switch thumb
866 */
867 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800868 if (mThumbDrawable == null) {
869 return false;
870 }
871
Alan Viverettecc2688d2013-09-17 17:00:12 -0700872 // Relies on mTempRect, MUST be called first!
873 final int thumbOffset = getThumbOffset();
874
Adam Powell12190b32010-11-28 19:07:53 -0800875 mThumbDrawable.getPadding(mTempRect);
876 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700877 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800878 final int thumbRight = thumbLeft + mThumbWidth +
879 mTempRect.left + mTempRect.right + mTouchSlop;
880 final int thumbBottom = mSwitchBottom + mTouchSlop;
881 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
882 }
883
884 @Override
885 public boolean onTouchEvent(MotionEvent ev) {
886 mVelocityTracker.addMovement(ev);
887 final int action = ev.getActionMasked();
888 switch (action) {
889 case MotionEvent.ACTION_DOWN: {
890 final float x = ev.getX();
891 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700892 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800893 mTouchMode = TOUCH_MODE_DOWN;
894 mTouchX = x;
895 mTouchY = y;
896 }
897 break;
898 }
899
900 case MotionEvent.ACTION_MOVE: {
901 switch (mTouchMode) {
902 case TOUCH_MODE_IDLE:
903 // Didn't target the thumb, treat normally.
904 break;
905
906 case TOUCH_MODE_DOWN: {
907 final float x = ev.getX();
908 final float y = ev.getY();
909 if (Math.abs(x - mTouchX) > mTouchSlop ||
910 Math.abs(y - mTouchY) > mTouchSlop) {
911 mTouchMode = TOUCH_MODE_DRAGGING;
912 getParent().requestDisallowInterceptTouchEvent(true);
913 mTouchX = x;
914 mTouchY = y;
915 return true;
916 }
917 break;
918 }
919
920 case TOUCH_MODE_DRAGGING: {
921 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700922 final int thumbScrollRange = getThumbScrollRange();
923 final float thumbScrollOffset = x - mTouchX;
924 float dPos;
925 if (thumbScrollRange != 0) {
926 dPos = thumbScrollOffset / thumbScrollRange;
927 } else {
928 // If the thumb scroll range is empty, just use the
929 // movement direction to snap on or off.
930 dPos = thumbScrollOffset > 0 ? 1 : -1;
931 }
932 if (isLayoutRtl()) {
933 dPos = -dPos;
934 }
935 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800936 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800937 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700938 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800939 }
940 return true;
941 }
942 }
943 break;
944 }
945
946 case MotionEvent.ACTION_UP:
947 case MotionEvent.ACTION_CANCEL: {
948 if (mTouchMode == TOUCH_MODE_DRAGGING) {
949 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700950 // Allow super class to handle pressed state, etc.
951 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800952 return true;
953 }
954 mTouchMode = TOUCH_MODE_IDLE;
955 mVelocityTracker.clear();
956 break;
957 }
958 }
959
960 return super.onTouchEvent(ev);
961 }
962
963 private void cancelSuperTouch(MotionEvent ev) {
964 MotionEvent cancel = MotionEvent.obtain(ev);
965 cancel.setAction(MotionEvent.ACTION_CANCEL);
966 super.onTouchEvent(cancel);
967 cancel.recycle();
968 }
969
970 /**
971 * Called from onTouchEvent to end a drag operation.
972 *
973 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
974 */
975 private void stopDrag(MotionEvent ev) {
976 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -0800977
Alan Viverette86453ff2013-09-26 14:46:08 -0700978 // Commit the change if the event is up and not canceled and the switch
979 // has not been disabled during the drag.
980 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -0700981 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -0700982 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -0800983 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -0800984 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -0700985 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -0800986 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700987 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -0800988 } else {
989 newState = getTargetCheckedState();
990 }
Adam Powell12190b32010-11-28 19:07:53 -0800991 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -0700992 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -0800993 }
Alan Viverette86453ff2013-09-26 14:46:08 -0700994
Alan Viveretted4e77902014-10-27 17:50:51 -0700995 if (newState != oldState) {
996 playSoundEffect(SoundEffectConstants.CLICK);
997 setChecked(newState);
998 }
999
Alan Viverette86453ff2013-09-26 14:46:08 -07001000 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001001 }
1002
1003 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001004 final float targetPosition = newCheckedState ? 1 : 0;
1005 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1006 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1007 mPositionAnimator.setAutoCancel(true);
1008 mPositionAnimator.start();
1009 }
1010
1011 private void cancelPositionAnimator() {
1012 if (mPositionAnimator != null) {
1013 mPositionAnimator.cancel();
1014 }
Adam Powell12190b32010-11-28 19:07:53 -08001015 }
1016
1017 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001018 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001019 }
1020
Alan Viverettecc2688d2013-09-17 17:00:12 -07001021 /**
1022 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1023 *
1024 * @param position new position between [0,1]
1025 */
1026 private void setThumbPosition(float position) {
1027 mThumbPosition = position;
1028 invalidate();
1029 }
1030
1031 @Override
1032 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001033 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001034 }
1035
1036 @Override
1037 public void setChecked(boolean checked) {
1038 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001039
Alan Viverette467d6292014-08-12 15:13:19 -07001040 // Calling the super method may result in setChecked() getting called
1041 // recursively with a different value, so load the REAL value...
1042 checked = isChecked();
1043
Alan Viverette86453ff2013-09-26 14:46:08 -07001044 if (isAttachedToWindow() && isLaidOut()) {
1045 animateThumbToCheckedState(checked);
1046 } else {
1047 // Immediately move the thumb to the new position.
1048 cancelPositionAnimator();
1049 setThumbPosition(checked ? 1 : 0);
1050 }
Adam Powell12190b32010-11-28 19:07:53 -08001051 }
1052
1053 @Override
1054 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1055 super.onLayout(changed, left, top, right, bottom);
1056
Alan Viverette0c0dde72014-07-30 13:29:39 -07001057 int opticalInsetLeft = 0;
1058 int opticalInsetRight = 0;
1059 if (mThumbDrawable != null) {
1060 final Rect trackPadding = mTempRect;
1061 if (mTrackDrawable != null) {
1062 mTrackDrawable.getPadding(trackPadding);
1063 } else {
1064 trackPadding.setEmpty();
1065 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001066
Alan Viverette0c0dde72014-07-30 13:29:39 -07001067 final Insets insets = mThumbDrawable.getOpticalInsets();
1068 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1069 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001070 }
1071
Alan Viverette0c0dde72014-07-30 13:29:39 -07001072 final int switchRight;
1073 final int switchLeft;
1074 if (isLayoutRtl()) {
1075 switchLeft = getPaddingLeft() + opticalInsetLeft;
1076 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1077 } else {
1078 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1079 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1080 }
1081
1082 final int switchTop;
1083 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001084 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1085 default:
1086 case Gravity.TOP:
1087 switchTop = getPaddingTop();
1088 switchBottom = switchTop + mSwitchHeight;
1089 break;
1090
1091 case Gravity.CENTER_VERTICAL:
1092 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1093 mSwitchHeight / 2;
1094 switchBottom = switchTop + mSwitchHeight;
1095 break;
1096
1097 case Gravity.BOTTOM:
1098 switchBottom = getHeight() - getPaddingBottom();
1099 switchTop = switchBottom - mSwitchHeight;
1100 break;
1101 }
1102
1103 mSwitchLeft = switchLeft;
1104 mSwitchTop = switchTop;
1105 mSwitchBottom = switchBottom;
1106 mSwitchRight = switchRight;
1107 }
1108
1109 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001110 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001111 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001112 final int switchLeft = mSwitchLeft;
1113 final int switchTop = mSwitchTop;
1114 final int switchRight = mSwitchRight;
1115 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001116
1117 int thumbInitialLeft = switchLeft + getThumbOffset();
1118
1119 final Insets thumbInsets;
1120 if (mThumbDrawable != null) {
1121 thumbInsets = mThumbDrawable.getOpticalInsets();
1122 } else {
1123 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001124 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001125
Alan Viverette0c0dde72014-07-30 13:29:39 -07001126 // Layout the track.
1127 if (mTrackDrawable != null) {
1128 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001129
Alan Viverette0c0dde72014-07-30 13:29:39 -07001130 // Adjust thumb position for track padding.
1131 thumbInitialLeft += padding.left;
1132
1133 // If necessary, offset by the optical insets of the thumb asset.
1134 int trackLeft = switchLeft;
1135 int trackTop = switchTop;
1136 int trackRight = switchRight;
1137 int trackBottom = switchBottom;
1138 if (thumbInsets != Insets.NONE) {
1139 if (thumbInsets.left > padding.left) {
1140 trackLeft += thumbInsets.left - padding.left;
1141 }
1142 if (thumbInsets.top > padding.top) {
1143 trackTop += thumbInsets.top - padding.top;
1144 }
1145 if (thumbInsets.right > padding.right) {
1146 trackRight -= thumbInsets.right - padding.right;
1147 }
1148 if (thumbInsets.bottom > padding.bottom) {
1149 trackBottom -= thumbInsets.bottom - padding.bottom;
1150 }
1151 }
1152 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1153 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001154
Alan Viverette661e6362014-05-12 10:55:37 -07001155 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001156 if (mThumbDrawable != null) {
1157 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001158
1159 final int thumbLeft = thumbInitialLeft - padding.left;
1160 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001161 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001162
Alan Viverette4d065a02014-07-11 15:28:38 -07001163 final Drawable background = getBackground();
1164 if (background != null) {
1165 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1166 }
Alan Viverette61956602014-04-22 19:07:06 -07001167 }
1168
Alan Viverettead2f8e32014-05-16 13:28:33 -07001169 // Draw the background.
1170 super.draw(c);
1171 }
1172
1173 @Override
1174 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001175 super.onDraw(canvas);
1176
Alan Viverette4d065a02014-07-11 15:28:38 -07001177 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001178 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001179 if (trackDrawable != null) {
1180 trackDrawable.getPadding(padding);
1181 } else {
1182 padding.setEmpty();
1183 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001184
1185 final int switchTop = mSwitchTop;
1186 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001187 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001188 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001189
Alan Viverette4d065a02014-07-11 15:28:38 -07001190 final Drawable thumbDrawable = mThumbDrawable;
1191 if (trackDrawable != null) {
1192 if (mSplitTrack && thumbDrawable != null) {
1193 final Insets insets = thumbDrawable.getOpticalInsets();
1194 thumbDrawable.copyBounds(padding);
1195 padding.left += insets.left;
1196 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001197
Alan Viverette4d065a02014-07-11 15:28:38 -07001198 final int saveCount = canvas.save();
1199 canvas.clipRect(padding, Op.DIFFERENCE);
1200 trackDrawable.draw(canvas);
1201 canvas.restoreToCount(saveCount);
1202 } else {
1203 trackDrawable.draw(canvas);
1204 }
Alan Viverette661e6362014-05-12 10:55:37 -07001205 }
Alan Viverette61956602014-04-22 19:07:06 -07001206
1207 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001208
1209 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001210 thumbDrawable.draw(canvas);
1211 }
Adam Powell12190b32010-11-28 19:07:53 -08001212
Alan Viverette5876ff42014-03-03 17:40:46 -08001213 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001214 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001215 final int drawableState[] = getDrawableState();
1216 if (mTextColors != null) {
1217 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1218 }
1219 mTextPaint.drawableState = drawableState;
1220
Alan Viverette4d065a02014-07-11 15:28:38 -07001221 final int cX;
1222 if (thumbDrawable != null) {
1223 final Rect bounds = thumbDrawable.getBounds();
1224 cX = bounds.left + bounds.right;
1225 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001226 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001227 }
1228
1229 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001230 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1231 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001232 switchText.draw(canvas);
1233 }
Adam Powell12190b32010-11-28 19:07:53 -08001234
Alan Viverette5876ff42014-03-03 17:40:46 -08001235 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001236 }
1237
1238 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001239 public int getCompoundPaddingLeft() {
1240 if (!isLayoutRtl()) {
1241 return super.getCompoundPaddingLeft();
1242 }
1243 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1244 if (!TextUtils.isEmpty(getText())) {
1245 padding += mSwitchPadding;
1246 }
1247 return padding;
1248 }
1249
1250 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001251 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001252 if (isLayoutRtl()) {
1253 return super.getCompoundPaddingRight();
1254 }
Adam Powell12190b32010-11-28 19:07:53 -08001255 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1256 if (!TextUtils.isEmpty(getText())) {
1257 padding += mSwitchPadding;
1258 }
1259 return padding;
1260 }
1261
Alan Viverettecc2688d2013-09-17 17:00:12 -07001262 /**
1263 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001264 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001265 *
1266 * @return thumb offset
1267 */
1268 private int getThumbOffset() {
1269 final float thumbPosition;
1270 if (isLayoutRtl()) {
1271 thumbPosition = 1 - mThumbPosition;
1272 } else {
1273 thumbPosition = mThumbPosition;
1274 }
1275 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1276 }
1277
Adam Powell12190b32010-11-28 19:07:53 -08001278 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001279 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001280 final Rect padding = mTempRect;
1281 mTrackDrawable.getPadding(padding);
1282
1283 final Insets insets;
1284 if (mThumbDrawable != null) {
1285 insets = mThumbDrawable.getOpticalInsets();
1286 } else {
1287 insets = Insets.NONE;
1288 }
1289
1290 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1291 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001292 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001293 return 0;
1294 }
Adam Powell12190b32010-11-28 19:07:53 -08001295 }
1296
1297 @Override
1298 protected int[] onCreateDrawableState(int extraSpace) {
1299 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1300 if (isChecked()) {
1301 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1302 }
1303 return drawableState;
1304 }
1305
1306 @Override
1307 protected void drawableStateChanged() {
1308 super.drawableStateChanged();
1309
Alan Viverette661e6362014-05-12 10:55:37 -07001310 final int[] myDrawableState = getDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -08001311
Alan Viverette2356c5e2014-05-22 22:43:59 -07001312 if (mThumbDrawable != null) {
1313 mThumbDrawable.setState(myDrawableState);
Alan Viverette661e6362014-05-12 10:55:37 -07001314 }
1315
1316 if (mTrackDrawable != null) {
1317 mTrackDrawable.setState(myDrawableState);
1318 }
Adam Powell12190b32010-11-28 19:07:53 -08001319
1320 invalidate();
1321 }
1322
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001323 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001324 public void drawableHotspotChanged(float x, float y) {
1325 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001326
1327 if (mThumbDrawable != null) {
1328 mThumbDrawable.setHotspot(x, y);
1329 }
1330
1331 if (mTrackDrawable != null) {
1332 mTrackDrawable.setHotspot(x, y);
1333 }
1334 }
1335
Adam Powell12190b32010-11-28 19:07:53 -08001336 @Override
1337 protected boolean verifyDrawable(Drawable who) {
1338 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1339 }
1340
1341 @Override
1342 public void jumpDrawablesToCurrentState() {
1343 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001344
1345 if (mThumbDrawable != null) {
1346 mThumbDrawable.jumpToCurrentState();
1347 }
1348
1349 if (mTrackDrawable != null) {
1350 mTrackDrawable.jumpToCurrentState();
1351 }
1352
1353 if (mPositionAnimator != null && mPositionAnimator.isRunning()) {
1354 mPositionAnimator.end();
1355 mPositionAnimator = null;
1356 }
Adam Powell12190b32010-11-28 19:07:53 -08001357 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001358
1359 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001360 public CharSequence getAccessibilityClassName() {
1361 return Switch.class.getName();
1362 }
1363
1364 @Override
Dianne Hackborn49b043f2015-05-07 14:21:38 -07001365 public void onProvideStructure(ViewStructure structure) {
1366 super.onProvideStructure(structure);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001367 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1368 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001369 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001370 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001371 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001372 } else {
1373 StringBuilder newText = new StringBuilder();
1374 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001375 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001376 }
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001377 structure.setTextPaint(mTextPaint);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001378 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001379 }
1380
Alan Viverettea54956a2015-01-07 16:05:02 -08001381 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001382 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001383 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1384 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001385 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1386 if (!TextUtils.isEmpty(switchText)) {
1387 CharSequence oldText = info.getText();
1388 if (TextUtils.isEmpty(oldText)) {
1389 info.setText(switchText);
1390 } else {
1391 StringBuilder newText = new StringBuilder();
1392 newText.append(oldText).append(' ').append(switchText);
1393 info.setText(newText);
1394 }
1395 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001396 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001397
1398 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1399 @Override
1400 public Float get(Switch object) {
1401 return object.mThumbPosition;
1402 }
1403
1404 @Override
1405 public void setValue(Switch object, float value) {
1406 object.setThumbPosition(value);
1407 }
1408 };
Adam Powell12190b32010-11-28 19:07:53 -08001409}