blob: eb81e6fb11821e0eb59494d192b5e131b628d62a [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 Viverettef6d87ec2016-03-11 10:09:14 -050021import android.annotation.NonNull;
Alan Viverettee7eee642015-01-29 14:27:49 -080022import android.annotation.Nullable;
Tor Norbye7b9c9122013-05-30 16:48:33 -070023import android.annotation.StyleRes;
Adam Powell12190b32010-11-28 19:07:53 -080024import android.content.Context;
25import android.content.res.ColorStateList;
26import android.content.res.Resources;
27import android.content.res.TypedArray;
28import android.graphics.Canvas;
Alan Viverette661e6362014-05-12 10:55:37 -070029import android.graphics.Insets;
Chris Craik6a49dde2015-05-12 10:28:14 -070030import android.graphics.Paint;
Alan Viverettee7eee642015-01-29 14:27:49 -080031import android.graphics.PorterDuff;
Adam Powell12190b32010-11-28 19:07:53 -080032import android.graphics.Rect;
33import android.graphics.Typeface;
Alan Viverette661e6362014-05-12 10:55:37 -070034import android.graphics.Region.Op;
Adam Powell12190b32010-11-28 19:07:53 -080035import android.graphics.drawable.Drawable;
36import android.text.Layout;
37import android.text.StaticLayout;
38import android.text.TextPaint;
39import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040040import android.text.method.AllCapsTransformationMethod;
41import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080042import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070043import android.util.FloatProperty;
44import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080045import android.view.Gravity;
46import android.view.MotionEvent;
Alan Viveretted4e77902014-10-27 17:50:51 -070047import android.view.SoundEffectConstants;
Adam Powell12190b32010-11-28 19:07:53 -080048import android.view.VelocityTracker;
Dianne Hackborn49b043f2015-05-07 14:21:38 -070049import android.view.ViewStructure;
Adam Powell12190b32010-11-28 19:07:53 -080050import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070051import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080052import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080053
Adam Powellbe0a4532010-11-29 17:47:48 -080054import com.android.internal.R;
55
Adam Powell12190b32010-11-28 19:07:53 -080056/**
57 * A Switch is a two-state toggle switch widget that can select between two
58 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070059 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
60 * property controls the text displayed in the label for the switch, whereas the
61 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
62 * controls the text on the thumb. Similarly, the
63 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
64 * setTypeface() methods control the typeface and style of label text, whereas the
65 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
Andrew Solovay07e70212015-07-08 12:49:21 -070066 * the related setSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080067 *
Scott Main4c359b72012-07-24 15:51:27 -070068 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
69 * guide.</p>
70 *
71 * @attr ref android.R.styleable#Switch_textOn
72 * @attr ref android.R.styleable#Switch_textOff
73 * @attr ref android.R.styleable#Switch_switchMinWidth
74 * @attr ref android.R.styleable#Switch_switchPadding
75 * @attr ref android.R.styleable#Switch_switchTextAppearance
76 * @attr ref android.R.styleable#Switch_thumb
77 * @attr ref android.R.styleable#Switch_thumbTextPadding
78 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080079 */
80public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070081 private static final int THUMB_ANIMATION_DURATION = 250;
82
Adam Powell12190b32010-11-28 19:07:53 -080083 private static final int TOUCH_MODE_IDLE = 0;
84 private static final int TOUCH_MODE_DOWN = 1;
85 private static final int TOUCH_MODE_DRAGGING = 2;
86
87 // Enum for the "typeface" XML parameter.
88 private static final int SANS = 1;
89 private static final int SERIF = 2;
90 private static final int MONOSPACE = 3;
91
92 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080093 private ColorStateList mThumbTintList = null;
94 private PorterDuff.Mode mThumbTintMode = null;
95 private boolean mHasThumbTint = false;
96 private boolean mHasThumbTintMode = false;
97
Adam Powell12190b32010-11-28 19:07:53 -080098 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080099 private ColorStateList mTrackTintList = null;
100 private PorterDuff.Mode mTrackTintMode = null;
101 private boolean mHasTrackTint = false;
102 private boolean mHasTrackTintMode = false;
103
Adam Powell12190b32010-11-28 19:07:53 -0800104 private int mThumbTextPadding;
105 private int mSwitchMinWidth;
106 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700107 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800108 private CharSequence mTextOn;
109 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700110 private boolean mShowText;
Adam Powell12190b32010-11-28 19:07:53 -0800111
112 private int mTouchMode;
113 private int mTouchSlop;
114 private float mTouchX;
115 private float mTouchY;
116 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
117 private int mMinFlingVelocity;
118
119 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700120
Alan Viverette0c0dde72014-07-30 13:29:39 -0700121 /**
122 * Width required to draw the switch track and thumb. Includes padding and
123 * optical bounds for both the track and thumb.
124 */
125 private int mSwitchWidth;
126
127 /**
128 * Height required to draw the switch track and thumb. Includes padding and
129 * optical bounds for both the track and thumb.
130 */
131 private int mSwitchHeight;
132
133 /**
134 * Width of the thumb's content region. Does not include padding or
135 * optical bounds.
136 */
137 private int mThumbWidth;
138
139 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800140 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700141
142 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800143 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700144
145 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800146 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700147
148 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800149 private int mSwitchBottom;
150
151 private TextPaint mTextPaint;
152 private ColorStateList mTextColors;
153 private Layout mOnLayout;
154 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400155 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700156 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800157
Adam Powellbe0a4532010-11-29 17:47:48 -0800158 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800159 private final Rect mTempRect = new Rect();
160
161 private static final int[] CHECKED_STATE_SET = {
162 R.attr.state_checked
163 };
164
165 /**
166 * Construct a new Switch with default styling.
167 *
168 * @param context The Context that will determine this widget's theming.
169 */
170 public Switch(Context context) {
171 this(context, null);
172 }
173
174 /**
175 * Construct a new Switch with default styling, overriding specific style
176 * 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 default styling.
180 */
181 public Switch(Context context, AttributeSet attrs) {
182 this(context, attrs, com.android.internal.R.attr.switchStyle);
183 }
184
185 /**
186 * Construct a new Switch with a default style determined by the given theme attribute,
187 * overriding specific style attributes as requested.
188 *
189 * @param context The Context that will determine this widget's theming.
190 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700191 * @param defStyleAttr An attribute in the current theme that contains a
192 * reference to a style resource that supplies default values for
193 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800194 */
Alan Viverette617feb92013-09-09 18:09:13 -0700195 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
196 this(context, attrs, defStyleAttr, 0);
197 }
198
199
200 /**
201 * Construct a new Switch with a default style determined by the given theme
202 * attribute or style resource, overriding specific style attributes as
203 * requested.
204 *
205 * @param context The Context that will determine this widget's theming.
206 * @param attrs Specification of attributes that should deviate from the
207 * default styling.
208 * @param defStyleAttr An attribute in the current theme that contains a
209 * reference to a style resource that supplies default values for
210 * the view. Can be 0 to not look for defaults.
211 * @param defStyleRes A resource identifier of a style resource that
212 * supplies default values for the view, used only if
213 * defStyleAttr is 0 or can not be found in the theme. Can be 0
214 * to not look for defaults.
215 */
216 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
217 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800218
Chris Craik6a49dde2015-05-12 10:28:14 -0700219 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700220
221 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800222 mTextPaint.density = res.getDisplayMetrics().density;
223 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
224
Alan Viverette617feb92013-09-09 18:09:13 -0700225 final TypedArray a = context.obtainStyledAttributes(
226 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700227 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700228 if (mThumbDrawable != null) {
229 mThumbDrawable.setCallback(this);
230 }
Chet Haase150176d2011-08-26 09:54:06 -0700231 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700232 if (mTrackDrawable != null) {
233 mTrackDrawable.setCallback(this);
234 }
Adam Powell12190b32010-11-28 19:07:53 -0800235 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
236 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700237 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800238 mThumbTextPadding = a.getDimensionPixelSize(
239 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
240 mSwitchMinWidth = a.getDimensionPixelSize(
241 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
242 mSwitchPadding = a.getDimensionPixelSize(
243 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700244 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800245
Jun Mukai9fb302c2015-06-24 18:31:06 -0700246 ColorStateList thumbTintList = a.getColorStateList(
247 com.android.internal.R.styleable.Switch_thumbTint);
248 if (thumbTintList != null) {
249 mThumbTintList = thumbTintList;
250 mHasThumbTint = true;
251 }
252 PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
253 a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
254 if (mThumbTintMode != thumbTintMode) {
255 mThumbTintMode = thumbTintMode;
256 mHasThumbTintMode = true;
257 }
258 if (mHasThumbTint || mHasThumbTintMode) {
259 applyThumbTint();
260 }
261
262 ColorStateList trackTintList = a.getColorStateList(
263 com.android.internal.R.styleable.Switch_trackTint);
264 if (trackTintList != null) {
265 mTrackTintList = trackTintList;
266 mHasTrackTint = true;
267 }
268 PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
269 a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
270 if (mTrackTintMode != trackTintMode) {
271 mTrackTintMode = trackTintMode;
272 mHasTrackTintMode = true;
273 }
274 if (mHasTrackTint || mHasTrackTintMode) {
275 applyTrackTint();
276 }
277
Alan Viverette661e6362014-05-12 10:55:37 -0700278 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800279 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
280 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700281 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800282 }
283 a.recycle();
284
Alan Viverette661e6362014-05-12 10:55:37 -0700285 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800286 mTouchSlop = config.getScaledTouchSlop();
287 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
288
289 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700290 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800291 setChecked(isChecked());
292 }
293
294 /**
295 * Sets the switch text color, size, style, hint color, and highlight color
296 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800297 *
298 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800299 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700300 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800301 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700302 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800303 com.android.internal.R.styleable.TextAppearance);
304
305 ColorStateList colors;
306 int ts;
307
308 colors = appearance.getColorStateList(com.android.internal.R.styleable.
309 TextAppearance_textColor);
310 if (colors != null) {
311 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700312 } else {
313 // If no color set in TextAppearance, default to the view's textColor
314 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800315 }
316
317 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
318 TextAppearance_textSize, 0);
319 if (ts != 0) {
320 if (ts != mTextPaint.getTextSize()) {
321 mTextPaint.setTextSize(ts);
322 requestLayout();
323 }
324 }
325
326 int typefaceIndex, styleIndex;
327
328 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
329 TextAppearance_typeface, -1);
330 styleIndex = appearance.getInt(com.android.internal.R.styleable.
331 TextAppearance_textStyle, -1);
332
333 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
334
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400335 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
336 TextAppearance_textAllCaps, false);
337 if (allCaps) {
338 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
339 mSwitchTransformationMethod.setLengthChangesAllowed(true);
340 } else {
341 mSwitchTransformationMethod = null;
342 }
343
Adam Powell12190b32010-11-28 19:07:53 -0800344 appearance.recycle();
345 }
346
347 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
348 Typeface tf = null;
349 switch (typefaceIndex) {
350 case SANS:
351 tf = Typeface.SANS_SERIF;
352 break;
353
354 case SERIF:
355 tf = Typeface.SERIF;
356 break;
357
358 case MONOSPACE:
359 tf = Typeface.MONOSPACE;
360 break;
361 }
362
363 setSwitchTypeface(tf, styleIndex);
364 }
365
366 /**
367 * Sets the typeface and style in which the text should be displayed on the
368 * switch, and turns on the fake bold and italic bits in the Paint if the
369 * Typeface that you provided does not have all the bits in the
370 * style that you specified.
371 */
372 public void setSwitchTypeface(Typeface tf, int style) {
373 if (style > 0) {
374 if (tf == null) {
375 tf = Typeface.defaultFromStyle(style);
376 } else {
377 tf = Typeface.create(tf, style);
378 }
379
380 setSwitchTypeface(tf);
381 // now compute what (if any) algorithmic styling is needed
382 int typefaceStyle = tf != null ? tf.getStyle() : 0;
383 int need = style & ~typefaceStyle;
384 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
385 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
386 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700387 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800388 mTextPaint.setTextSkewX(0);
389 setSwitchTypeface(tf);
390 }
391 }
392
393 /**
Chet Haase150176d2011-08-26 09:54:06 -0700394 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800395 * Note that not all Typeface families actually have bold and italic
396 * variants, so you may need to use
397 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
398 * that you actually want.
399 *
400 * @attr ref android.R.styleable#TextView_typeface
401 * @attr ref android.R.styleable#TextView_textStyle
402 */
403 public void setSwitchTypeface(Typeface tf) {
404 if (mTextPaint.getTypeface() != tf) {
405 mTextPaint.setTypeface(tf);
406
407 requestLayout();
408 invalidate();
409 }
410 }
411
412 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800413 * Set the amount of horizontal padding between the switch and the associated text.
414 *
415 * @param pixels Amount of padding in pixels
416 *
417 * @attr ref android.R.styleable#Switch_switchPadding
418 */
419 public void setSwitchPadding(int pixels) {
420 mSwitchPadding = pixels;
421 requestLayout();
422 }
423
424 /**
425 * Get the amount of horizontal padding between the switch and the associated text.
426 *
427 * @return Amount of padding in pixels
428 *
429 * @attr ref android.R.styleable#Switch_switchPadding
430 */
431 public int getSwitchPadding() {
432 return mSwitchPadding;
433 }
434
435 /**
436 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
437 * of this value and its measured width as determined by the switch drawables and text used.
438 *
439 * @param pixels Minimum width of the switch in pixels
440 *
441 * @attr ref android.R.styleable#Switch_switchMinWidth
442 */
443 public void setSwitchMinWidth(int pixels) {
444 mSwitchMinWidth = pixels;
445 requestLayout();
446 }
447
448 /**
449 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
450 * of this value and its measured width as determined by the switch drawables and text used.
451 *
452 * @return Minimum width of the switch in pixels
453 *
454 * @attr ref android.R.styleable#Switch_switchMinWidth
455 */
456 public int getSwitchMinWidth() {
457 return mSwitchMinWidth;
458 }
459
460 /**
461 * Set the horizontal padding around the text drawn on the switch itself.
462 *
463 * @param pixels Horizontal padding for switch thumb text in pixels
464 *
465 * @attr ref android.R.styleable#Switch_thumbTextPadding
466 */
467 public void setThumbTextPadding(int pixels) {
468 mThumbTextPadding = pixels;
469 requestLayout();
470 }
471
472 /**
473 * Get the horizontal padding around the text drawn on the switch itself.
474 *
475 * @return Horizontal padding for switch thumb text in pixels
476 *
477 * @attr ref android.R.styleable#Switch_thumbTextPadding
478 */
479 public int getThumbTextPadding() {
480 return mThumbTextPadding;
481 }
482
483 /**
484 * Set the drawable used for the track that the switch slides within.
485 *
486 * @param track Track drawable
487 *
488 * @attr ref android.R.styleable#Switch_track
489 */
490 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700491 if (mTrackDrawable != null) {
492 mTrackDrawable.setCallback(null);
493 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800494 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700495 if (track != null) {
496 track.setCallback(this);
497 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800498 requestLayout();
499 }
500
501 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800502 * Set the drawable used for the track that the switch slides within.
503 *
Adam Powelldca510e2012-03-08 20:06:39 -0800504 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800505 *
506 * @attr ref android.R.styleable#Switch_track
507 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700508 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800509 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800510 }
511
512 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800513 * Get the drawable used for the track that the switch slides within.
514 *
515 * @return Track drawable
516 *
517 * @attr ref android.R.styleable#Switch_track
518 */
519 public Drawable getTrackDrawable() {
520 return mTrackDrawable;
521 }
522
523 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800524 * Applies a tint to the track drawable. Does not modify the current
525 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
526 * <p>
527 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
528 * automatically mutate the drawable and apply the specified tint and tint
529 * mode using {@link Drawable#setTintList(ColorStateList)}.
530 *
531 * @param tint the tint to apply, may be {@code null} to clear tint
532 *
533 * @attr ref android.R.styleable#Switch_trackTint
534 * @see #getTrackTintList()
535 * @see Drawable#setTintList(ColorStateList)
536 */
537 public void setTrackTintList(@Nullable ColorStateList tint) {
538 mTrackTintList = tint;
539 mHasTrackTint = true;
540
541 applyTrackTint();
542 }
543
544 /**
545 * @return the tint applied to the track drawable
546 * @attr ref android.R.styleable#Switch_trackTint
547 * @see #setTrackTintList(ColorStateList)
548 */
549 @Nullable
550 public ColorStateList getTrackTintList() {
551 return mTrackTintList;
552 }
553
554 /**
555 * Specifies the blending mode used to apply the tint specified by
556 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
557 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
558 *
559 * @param tintMode the blending mode used to apply the tint, may be
560 * {@code null} to clear tint
561 * @attr ref android.R.styleable#Switch_trackTintMode
562 * @see #getTrackTintMode()
563 * @see Drawable#setTintMode(PorterDuff.Mode)
564 */
565 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
566 mTrackTintMode = tintMode;
567 mHasTrackTintMode = true;
568
569 applyTrackTint();
570 }
571
572 /**
573 * @return the blending mode used to apply the tint to the track
574 * drawable
575 * @attr ref android.R.styleable#Switch_trackTintMode
576 * @see #setTrackTintMode(PorterDuff.Mode)
577 */
578 @Nullable
579 public PorterDuff.Mode getTrackTintMode() {
580 return mTrackTintMode;
581 }
582
583 private void applyTrackTint() {
584 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
585 mTrackDrawable = mTrackDrawable.mutate();
586
587 if (mHasTrackTint) {
588 mTrackDrawable.setTintList(mTrackTintList);
589 }
590
591 if (mHasTrackTintMode) {
592 mTrackDrawable.setTintMode(mTrackTintMode);
593 }
594
595 // The drawable (or one of its children) may not have been
596 // stateful before applying the tint, so let's try again.
597 if (mTrackDrawable.isStateful()) {
598 mTrackDrawable.setState(getDrawableState());
599 }
600 }
601 }
602
603 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800604 * Set the drawable used for the switch "thumb" - the piece that the user
605 * can physically touch and drag along the track.
606 *
607 * @param thumb Thumb drawable
608 *
609 * @attr ref android.R.styleable#Switch_thumb
610 */
611 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700612 if (mThumbDrawable != null) {
613 mThumbDrawable.setCallback(null);
614 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800615 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700616 if (thumb != null) {
617 thumb.setCallback(this);
618 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800619 requestLayout();
620 }
621
622 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800623 * Set the drawable used for the switch "thumb" - the piece that the user
624 * can physically touch and drag along the track.
625 *
Adam Powelldca510e2012-03-08 20:06:39 -0800626 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800627 *
628 * @attr ref android.R.styleable#Switch_thumb
629 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700630 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800631 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800632 }
633
634 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800635 * Get the drawable used for the switch "thumb" - the piece that the user
636 * can physically touch and drag along the track.
637 *
638 * @return Thumb drawable
639 *
640 * @attr ref android.R.styleable#Switch_thumb
641 */
642 public Drawable getThumbDrawable() {
643 return mThumbDrawable;
644 }
645
646 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800647 * Applies a tint to the thumb drawable. Does not modify the current
648 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
649 * <p>
650 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
651 * automatically mutate the drawable and apply the specified tint and tint
652 * mode using {@link Drawable#setTintList(ColorStateList)}.
653 *
654 * @param tint the tint to apply, may be {@code null} to clear tint
655 *
656 * @attr ref android.R.styleable#Switch_thumbTint
657 * @see #getThumbTintList()
658 * @see Drawable#setTintList(ColorStateList)
659 */
660 public void setThumbTintList(@Nullable ColorStateList tint) {
661 mThumbTintList = tint;
662 mHasThumbTint = true;
663
664 applyThumbTint();
665 }
666
667 /**
668 * @return the tint applied to the thumb drawable
669 * @attr ref android.R.styleable#Switch_thumbTint
670 * @see #setThumbTintList(ColorStateList)
671 */
672 @Nullable
673 public ColorStateList getThumbTintList() {
674 return mThumbTintList;
675 }
676
677 /**
678 * Specifies the blending mode used to apply the tint specified by
679 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
680 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
681 *
682 * @param tintMode the blending mode used to apply the tint, may be
683 * {@code null} to clear tint
684 * @attr ref android.R.styleable#Switch_thumbTintMode
685 * @see #getThumbTintMode()
686 * @see Drawable#setTintMode(PorterDuff.Mode)
687 */
688 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
689 mThumbTintMode = tintMode;
690 mHasThumbTintMode = true;
691
692 applyThumbTint();
693 }
694
695 /**
696 * @return the blending mode used to apply the tint to the thumb
697 * drawable
698 * @attr ref android.R.styleable#Switch_thumbTintMode
699 * @see #setThumbTintMode(PorterDuff.Mode)
700 */
701 @Nullable
702 public PorterDuff.Mode getThumbTintMode() {
703 return mThumbTintMode;
704 }
705
706 private void applyThumbTint() {
707 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
708 mThumbDrawable = mThumbDrawable.mutate();
709
710 if (mHasThumbTint) {
711 mThumbDrawable.setTintList(mThumbTintList);
712 }
713
714 if (mHasThumbTintMode) {
715 mThumbDrawable.setTintMode(mThumbTintMode);
716 }
717
718 // The drawable (or one of its children) may not have been
719 // stateful before applying the tint, so let's try again.
720 if (mThumbDrawable.isStateful()) {
721 mThumbDrawable.setState(getDrawableState());
722 }
723 }
724 }
725
726 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700727 * Specifies whether the track should be split by the thumb. When true,
728 * the thumb's optical bounds will be clipped out of the track drawable,
729 * then the thumb will be drawn into the resulting gap.
730 *
731 * @param splitTrack Whether the track should be split by the thumb
732 *
733 * @attr ref android.R.styleable#Switch_splitTrack
734 */
735 public void setSplitTrack(boolean splitTrack) {
736 mSplitTrack = splitTrack;
737 invalidate();
738 }
739
740 /**
741 * Returns whether the track should be split by the thumb.
742 *
743 * @attr ref android.R.styleable#Switch_splitTrack
744 */
745 public boolean getSplitTrack() {
746 return mSplitTrack;
747 }
748
749 /**
Chet Haase150176d2011-08-26 09:54:06 -0700750 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800751 *
752 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800753 */
754 public CharSequence getTextOn() {
755 return mTextOn;
756 }
757
758 /**
Chet Haase150176d2011-08-26 09:54:06 -0700759 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800760 *
761 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800762 */
763 public void setTextOn(CharSequence textOn) {
764 mTextOn = textOn;
765 requestLayout();
766 }
767
768 /**
Chet Haase150176d2011-08-26 09:54:06 -0700769 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800770 *
771 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800772 */
773 public CharSequence getTextOff() {
774 return mTextOff;
775 }
776
777 /**
Chet Haase150176d2011-08-26 09:54:06 -0700778 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800779 *
780 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800781 */
782 public void setTextOff(CharSequence textOff) {
783 mTextOff = textOff;
784 requestLayout();
785 }
786
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700787 /**
788 * Sets whether the on/off text should be displayed.
789 *
790 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700791 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700792 */
793 public void setShowText(boolean showText) {
794 if (mShowText != showText) {
795 mShowText = showText;
796 requestLayout();
797 }
798 }
799
800 /**
801 * @return whether the on/off text should be displayed
Alan Viverette0c0dde72014-07-30 13:29:39 -0700802 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700803 */
804 public boolean getShowText() {
805 return mShowText;
806 }
807
Adam Powell12190b32010-11-28 19:07:53 -0800808 @Override
809 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700810 if (mShowText) {
811 if (mOnLayout == null) {
812 mOnLayout = makeLayout(mTextOn);
813 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800814
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700815 if (mOffLayout == null) {
816 mOffLayout = makeLayout(mTextOff);
817 }
Adam Powell12190b32010-11-28 19:07:53 -0800818 }
819
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000820 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700821 final int thumbWidth;
822 final int thumbHeight;
823 if (mThumbDrawable != null) {
824 // Cached thumb width does not include padding.
825 mThumbDrawable.getPadding(padding);
826 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
827 thumbHeight = mThumbDrawable.getIntrinsicHeight();
828 } else {
829 thumbWidth = 0;
830 thumbHeight = 0;
831 }
832
833 final int maxTextWidth;
834 if (mShowText) {
835 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
836 + mThumbTextPadding * 2;
837 } else {
838 maxTextWidth = 0;
839 }
840
841 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
842
843 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700844 if (mTrackDrawable != null) {
845 mTrackDrawable.getPadding(padding);
846 trackHeight = mTrackDrawable.getIntrinsicHeight();
847 } else {
848 padding.setEmpty();
849 trackHeight = 0;
850 }
851
Alan Viverette0c0dde72014-07-30 13:29:39 -0700852 // Adjust left and right padding to ensure there's enough room for the
853 // thumb's padding (when present).
854 int paddingLeft = padding.left;
855 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700856 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700857 final Insets inset = mThumbDrawable.getOpticalInsets();
858 paddingLeft = Math.max(paddingLeft, inset.left);
859 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700860 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800861
Adam Powell12190b32010-11-28 19:07:53 -0800862 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700863 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700864 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800865 mSwitchWidth = switchWidth;
866 mSwitchHeight = switchHeight;
867
868 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700869
Adam Powell12190b32010-11-28 19:07:53 -0800870 final int measuredHeight = getMeasuredHeight();
871 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800872 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800873 }
874 }
875
Alan Viverettea54956a2015-01-07 16:05:02 -0800876 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700877 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800878 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
879 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700880
881 final CharSequence text = isChecked() ? mTextOn : mTextOff;
882 if (text != null) {
883 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700884 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700885 }
886
Adam Powell12190b32010-11-28 19:07:53 -0800887 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400888 final CharSequence transformed = (mSwitchTransformationMethod != null)
889 ? mSwitchTransformationMethod.getTransformation(text, this)
890 : text;
891
Siyamed Sinir79bf9d12016-05-18 19:57:52 -0700892 int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
893 transformed.length(), mTextPaint, getTextDirectionHeuristic()));
894 return new StaticLayout(transformed, mTextPaint, width,
Adam Powell12190b32010-11-28 19:07:53 -0800895 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
896 }
897
898 /**
899 * @return true if (x, y) is within the target area of the switch thumb
900 */
901 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800902 if (mThumbDrawable == null) {
903 return false;
904 }
905
Alan Viverettecc2688d2013-09-17 17:00:12 -0700906 // Relies on mTempRect, MUST be called first!
907 final int thumbOffset = getThumbOffset();
908
Adam Powell12190b32010-11-28 19:07:53 -0800909 mThumbDrawable.getPadding(mTempRect);
910 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700911 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800912 final int thumbRight = thumbLeft + mThumbWidth +
913 mTempRect.left + mTempRect.right + mTouchSlop;
914 final int thumbBottom = mSwitchBottom + mTouchSlop;
915 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
916 }
917
918 @Override
919 public boolean onTouchEvent(MotionEvent ev) {
920 mVelocityTracker.addMovement(ev);
921 final int action = ev.getActionMasked();
922 switch (action) {
923 case MotionEvent.ACTION_DOWN: {
924 final float x = ev.getX();
925 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700926 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800927 mTouchMode = TOUCH_MODE_DOWN;
928 mTouchX = x;
929 mTouchY = y;
930 }
931 break;
932 }
933
934 case MotionEvent.ACTION_MOVE: {
935 switch (mTouchMode) {
936 case TOUCH_MODE_IDLE:
937 // Didn't target the thumb, treat normally.
938 break;
939
940 case TOUCH_MODE_DOWN: {
941 final float x = ev.getX();
942 final float y = ev.getY();
943 if (Math.abs(x - mTouchX) > mTouchSlop ||
944 Math.abs(y - mTouchY) > mTouchSlop) {
945 mTouchMode = TOUCH_MODE_DRAGGING;
946 getParent().requestDisallowInterceptTouchEvent(true);
947 mTouchX = x;
948 mTouchY = y;
949 return true;
950 }
951 break;
952 }
953
954 case TOUCH_MODE_DRAGGING: {
955 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700956 final int thumbScrollRange = getThumbScrollRange();
957 final float thumbScrollOffset = x - mTouchX;
958 float dPos;
959 if (thumbScrollRange != 0) {
960 dPos = thumbScrollOffset / thumbScrollRange;
961 } else {
962 // If the thumb scroll range is empty, just use the
963 // movement direction to snap on or off.
964 dPos = thumbScrollOffset > 0 ? 1 : -1;
965 }
966 if (isLayoutRtl()) {
967 dPos = -dPos;
968 }
969 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800970 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800971 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700972 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800973 }
974 return true;
975 }
976 }
977 break;
978 }
979
980 case MotionEvent.ACTION_UP:
981 case MotionEvent.ACTION_CANCEL: {
982 if (mTouchMode == TOUCH_MODE_DRAGGING) {
983 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700984 // Allow super class to handle pressed state, etc.
985 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800986 return true;
987 }
988 mTouchMode = TOUCH_MODE_IDLE;
989 mVelocityTracker.clear();
990 break;
991 }
992 }
993
994 return super.onTouchEvent(ev);
995 }
996
997 private void cancelSuperTouch(MotionEvent ev) {
998 MotionEvent cancel = MotionEvent.obtain(ev);
999 cancel.setAction(MotionEvent.ACTION_CANCEL);
1000 super.onTouchEvent(cancel);
1001 cancel.recycle();
1002 }
1003
1004 /**
1005 * Called from onTouchEvent to end a drag operation.
1006 *
1007 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1008 */
1009 private void stopDrag(MotionEvent ev) {
1010 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001011
Alan Viverette86453ff2013-09-26 14:46:08 -07001012 // Commit the change if the event is up and not canceled and the switch
1013 // has not been disabled during the drag.
1014 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001015 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001016 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001017 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001018 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001019 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001020 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001021 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001022 } else {
1023 newState = getTargetCheckedState();
1024 }
Adam Powell12190b32010-11-28 19:07:53 -08001025 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001026 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001027 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001028
Alan Viveretted4e77902014-10-27 17:50:51 -07001029 if (newState != oldState) {
1030 playSoundEffect(SoundEffectConstants.CLICK);
Alan Viveretted4e77902014-10-27 17:50:51 -07001031 }
Alan Viverette03306a02015-08-07 11:34:25 -04001032 // Always call setChecked so that the thumb is moved back to the correct edge
1033 setChecked(newState);
Alan Viverette86453ff2013-09-26 14:46:08 -07001034 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001035 }
1036
1037 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001038 final float targetPosition = newCheckedState ? 1 : 0;
1039 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1040 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1041 mPositionAnimator.setAutoCancel(true);
1042 mPositionAnimator.start();
1043 }
1044
1045 private void cancelPositionAnimator() {
1046 if (mPositionAnimator != null) {
1047 mPositionAnimator.cancel();
1048 }
Adam Powell12190b32010-11-28 19:07:53 -08001049 }
1050
1051 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001052 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001053 }
1054
Alan Viverettecc2688d2013-09-17 17:00:12 -07001055 /**
1056 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1057 *
1058 * @param position new position between [0,1]
1059 */
1060 private void setThumbPosition(float position) {
1061 mThumbPosition = position;
1062 invalidate();
1063 }
1064
1065 @Override
1066 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001067 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001068 }
1069
1070 @Override
1071 public void setChecked(boolean checked) {
1072 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001073
Alan Viverette467d6292014-08-12 15:13:19 -07001074 // Calling the super method may result in setChecked() getting called
1075 // recursively with a different value, so load the REAL value...
1076 checked = isChecked();
1077
Alan Viverette86453ff2013-09-26 14:46:08 -07001078 if (isAttachedToWindow() && isLaidOut()) {
1079 animateThumbToCheckedState(checked);
1080 } else {
1081 // Immediately move the thumb to the new position.
1082 cancelPositionAnimator();
1083 setThumbPosition(checked ? 1 : 0);
1084 }
Adam Powell12190b32010-11-28 19:07:53 -08001085 }
1086
1087 @Override
1088 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1089 super.onLayout(changed, left, top, right, bottom);
1090
Alan Viverette0c0dde72014-07-30 13:29:39 -07001091 int opticalInsetLeft = 0;
1092 int opticalInsetRight = 0;
1093 if (mThumbDrawable != null) {
1094 final Rect trackPadding = mTempRect;
1095 if (mTrackDrawable != null) {
1096 mTrackDrawable.getPadding(trackPadding);
1097 } else {
1098 trackPadding.setEmpty();
1099 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001100
Alan Viverette0c0dde72014-07-30 13:29:39 -07001101 final Insets insets = mThumbDrawable.getOpticalInsets();
1102 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1103 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001104 }
1105
Alan Viverette0c0dde72014-07-30 13:29:39 -07001106 final int switchRight;
1107 final int switchLeft;
1108 if (isLayoutRtl()) {
1109 switchLeft = getPaddingLeft() + opticalInsetLeft;
1110 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1111 } else {
1112 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1113 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1114 }
1115
1116 final int switchTop;
1117 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001118 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1119 default:
1120 case Gravity.TOP:
1121 switchTop = getPaddingTop();
1122 switchBottom = switchTop + mSwitchHeight;
1123 break;
1124
1125 case Gravity.CENTER_VERTICAL:
1126 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1127 mSwitchHeight / 2;
1128 switchBottom = switchTop + mSwitchHeight;
1129 break;
1130
1131 case Gravity.BOTTOM:
1132 switchBottom = getHeight() - getPaddingBottom();
1133 switchTop = switchBottom - mSwitchHeight;
1134 break;
1135 }
1136
1137 mSwitchLeft = switchLeft;
1138 mSwitchTop = switchTop;
1139 mSwitchBottom = switchBottom;
1140 mSwitchRight = switchRight;
1141 }
1142
1143 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001144 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001145 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001146 final int switchLeft = mSwitchLeft;
1147 final int switchTop = mSwitchTop;
1148 final int switchRight = mSwitchRight;
1149 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001150
1151 int thumbInitialLeft = switchLeft + getThumbOffset();
1152
1153 final Insets thumbInsets;
1154 if (mThumbDrawable != null) {
1155 thumbInsets = mThumbDrawable.getOpticalInsets();
1156 } else {
1157 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001158 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001159
Alan Viverette0c0dde72014-07-30 13:29:39 -07001160 // Layout the track.
1161 if (mTrackDrawable != null) {
1162 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001163
Alan Viverette0c0dde72014-07-30 13:29:39 -07001164 // Adjust thumb position for track padding.
1165 thumbInitialLeft += padding.left;
1166
1167 // If necessary, offset by the optical insets of the thumb asset.
1168 int trackLeft = switchLeft;
1169 int trackTop = switchTop;
1170 int trackRight = switchRight;
1171 int trackBottom = switchBottom;
1172 if (thumbInsets != Insets.NONE) {
1173 if (thumbInsets.left > padding.left) {
1174 trackLeft += thumbInsets.left - padding.left;
1175 }
1176 if (thumbInsets.top > padding.top) {
1177 trackTop += thumbInsets.top - padding.top;
1178 }
1179 if (thumbInsets.right > padding.right) {
1180 trackRight -= thumbInsets.right - padding.right;
1181 }
1182 if (thumbInsets.bottom > padding.bottom) {
1183 trackBottom -= thumbInsets.bottom - padding.bottom;
1184 }
1185 }
1186 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1187 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001188
Alan Viverette661e6362014-05-12 10:55:37 -07001189 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001190 if (mThumbDrawable != null) {
1191 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001192
1193 final int thumbLeft = thumbInitialLeft - padding.left;
1194 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001195 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001196
Alan Viverette4d065a02014-07-11 15:28:38 -07001197 final Drawable background = getBackground();
1198 if (background != null) {
1199 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1200 }
Alan Viverette61956602014-04-22 19:07:06 -07001201 }
1202
Alan Viverettead2f8e32014-05-16 13:28:33 -07001203 // Draw the background.
1204 super.draw(c);
1205 }
1206
1207 @Override
1208 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001209 super.onDraw(canvas);
1210
Alan Viverette4d065a02014-07-11 15:28:38 -07001211 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001212 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001213 if (trackDrawable != null) {
1214 trackDrawable.getPadding(padding);
1215 } else {
1216 padding.setEmpty();
1217 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001218
1219 final int switchTop = mSwitchTop;
1220 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001221 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001222 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001223
Alan Viverette4d065a02014-07-11 15:28:38 -07001224 final Drawable thumbDrawable = mThumbDrawable;
1225 if (trackDrawable != null) {
1226 if (mSplitTrack && thumbDrawable != null) {
1227 final Insets insets = thumbDrawable.getOpticalInsets();
1228 thumbDrawable.copyBounds(padding);
1229 padding.left += insets.left;
1230 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001231
Alan Viverette4d065a02014-07-11 15:28:38 -07001232 final int saveCount = canvas.save();
1233 canvas.clipRect(padding, Op.DIFFERENCE);
1234 trackDrawable.draw(canvas);
1235 canvas.restoreToCount(saveCount);
1236 } else {
1237 trackDrawable.draw(canvas);
1238 }
Alan Viverette661e6362014-05-12 10:55:37 -07001239 }
Alan Viverette61956602014-04-22 19:07:06 -07001240
1241 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001242
1243 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001244 thumbDrawable.draw(canvas);
1245 }
Adam Powell12190b32010-11-28 19:07:53 -08001246
Alan Viverette5876ff42014-03-03 17:40:46 -08001247 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001248 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001249 final int drawableState[] = getDrawableState();
1250 if (mTextColors != null) {
1251 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1252 }
1253 mTextPaint.drawableState = drawableState;
1254
Alan Viverette4d065a02014-07-11 15:28:38 -07001255 final int cX;
1256 if (thumbDrawable != null) {
1257 final Rect bounds = thumbDrawable.getBounds();
1258 cX = bounds.left + bounds.right;
1259 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001260 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001261 }
1262
1263 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001264 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1265 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001266 switchText.draw(canvas);
1267 }
Adam Powell12190b32010-11-28 19:07:53 -08001268
Alan Viverette5876ff42014-03-03 17:40:46 -08001269 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001270 }
1271
1272 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001273 public int getCompoundPaddingLeft() {
1274 if (!isLayoutRtl()) {
1275 return super.getCompoundPaddingLeft();
1276 }
1277 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1278 if (!TextUtils.isEmpty(getText())) {
1279 padding += mSwitchPadding;
1280 }
1281 return padding;
1282 }
1283
1284 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001285 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001286 if (isLayoutRtl()) {
1287 return super.getCompoundPaddingRight();
1288 }
Adam Powell12190b32010-11-28 19:07:53 -08001289 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1290 if (!TextUtils.isEmpty(getText())) {
1291 padding += mSwitchPadding;
1292 }
1293 return padding;
1294 }
1295
Alan Viverettecc2688d2013-09-17 17:00:12 -07001296 /**
1297 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001298 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001299 *
1300 * @return thumb offset
1301 */
1302 private int getThumbOffset() {
1303 final float thumbPosition;
1304 if (isLayoutRtl()) {
1305 thumbPosition = 1 - mThumbPosition;
1306 } else {
1307 thumbPosition = mThumbPosition;
1308 }
1309 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1310 }
1311
Adam Powell12190b32010-11-28 19:07:53 -08001312 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001313 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001314 final Rect padding = mTempRect;
1315 mTrackDrawable.getPadding(padding);
1316
1317 final Insets insets;
1318 if (mThumbDrawable != null) {
1319 insets = mThumbDrawable.getOpticalInsets();
1320 } else {
1321 insets = Insets.NONE;
1322 }
1323
1324 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1325 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001326 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001327 return 0;
1328 }
Adam Powell12190b32010-11-28 19:07:53 -08001329 }
1330
1331 @Override
1332 protected int[] onCreateDrawableState(int extraSpace) {
1333 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1334 if (isChecked()) {
1335 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1336 }
1337 return drawableState;
1338 }
1339
1340 @Override
1341 protected void drawableStateChanged() {
1342 super.drawableStateChanged();
1343
Alan Viverettead0020f2015-09-04 10:10:42 -04001344 final int[] state = getDrawableState();
1345 boolean changed = false;
Adam Powell12190b32010-11-28 19:07:53 -08001346
Alan Viverettead0020f2015-09-04 10:10:42 -04001347 final Drawable thumbDrawable = mThumbDrawable;
1348 if (thumbDrawable != null && thumbDrawable.isStateful()) {
1349 changed |= thumbDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001350 }
1351
Alan Viverettead0020f2015-09-04 10:10:42 -04001352 final Drawable trackDrawable = mTrackDrawable;
1353 if (trackDrawable != null && trackDrawable.isStateful()) {
1354 changed |= trackDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001355 }
Adam Powell12190b32010-11-28 19:07:53 -08001356
Alan Viverettead0020f2015-09-04 10:10:42 -04001357 if (changed) {
1358 invalidate();
1359 }
Adam Powell12190b32010-11-28 19:07:53 -08001360 }
1361
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001362 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001363 public void drawableHotspotChanged(float x, float y) {
1364 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001365
1366 if (mThumbDrawable != null) {
1367 mThumbDrawable.setHotspot(x, y);
1368 }
1369
1370 if (mTrackDrawable != null) {
1371 mTrackDrawable.setHotspot(x, y);
1372 }
1373 }
1374
Adam Powell12190b32010-11-28 19:07:53 -08001375 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -05001376 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell12190b32010-11-28 19:07:53 -08001377 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1378 }
1379
1380 @Override
1381 public void jumpDrawablesToCurrentState() {
1382 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001383
1384 if (mThumbDrawable != null) {
1385 mThumbDrawable.jumpToCurrentState();
1386 }
1387
1388 if (mTrackDrawable != null) {
1389 mTrackDrawable.jumpToCurrentState();
1390 }
1391
Alan Viveretteee20d772015-09-23 14:23:21 -04001392 if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001393 mPositionAnimator.end();
1394 mPositionAnimator = null;
1395 }
Adam Powell12190b32010-11-28 19:07:53 -08001396 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001397
1398 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001399 public CharSequence getAccessibilityClassName() {
1400 return Switch.class.getName();
1401 }
1402
1403 @Override
Dianne Hackborn49b043f2015-05-07 14:21:38 -07001404 public void onProvideStructure(ViewStructure structure) {
1405 super.onProvideStructure(structure);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001406 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1407 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001408 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001409 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001410 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001411 } else {
1412 StringBuilder newText = new StringBuilder();
1413 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001414 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001415 }
James Cook5cfaae42015-05-28 15:52:44 -07001416 // The style of the label text is provided via the base TextView class. This is more
1417 // relevant than the style of the (optional) on/off text on the switch button itself,
1418 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001419 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001420 }
1421
Alan Viverettea54956a2015-01-07 16:05:02 -08001422 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001423 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001424 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1425 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001426 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1427 if (!TextUtils.isEmpty(switchText)) {
1428 CharSequence oldText = info.getText();
1429 if (TextUtils.isEmpty(oldText)) {
1430 info.setText(switchText);
1431 } else {
1432 StringBuilder newText = new StringBuilder();
1433 newText.append(oldText).append(' ').append(switchText);
1434 info.setText(newText);
1435 }
1436 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001437 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001438
1439 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1440 @Override
1441 public Float get(Switch object) {
1442 return object.mThumbPosition;
1443 }
1444
1445 @Override
1446 public void setValue(Switch object, float value) {
1447 object.setThumbPosition(value);
1448 }
1449 };
Adam Powell12190b32010-11-28 19:07:53 -08001450}