blob: c4a17715896c1b3ed89b01eb4a9346eefbdc5186 [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
892 return new StaticLayout(transformed, mTextPaint,
893 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800894 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
895 }
896
897 /**
898 * @return true if (x, y) is within the target area of the switch thumb
899 */
900 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800901 if (mThumbDrawable == null) {
902 return false;
903 }
904
Alan Viverettecc2688d2013-09-17 17:00:12 -0700905 // Relies on mTempRect, MUST be called first!
906 final int thumbOffset = getThumbOffset();
907
Adam Powell12190b32010-11-28 19:07:53 -0800908 mThumbDrawable.getPadding(mTempRect);
909 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700910 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800911 final int thumbRight = thumbLeft + mThumbWidth +
912 mTempRect.left + mTempRect.right + mTouchSlop;
913 final int thumbBottom = mSwitchBottom + mTouchSlop;
914 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
915 }
916
917 @Override
918 public boolean onTouchEvent(MotionEvent ev) {
919 mVelocityTracker.addMovement(ev);
920 final int action = ev.getActionMasked();
921 switch (action) {
922 case MotionEvent.ACTION_DOWN: {
923 final float x = ev.getX();
924 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700925 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800926 mTouchMode = TOUCH_MODE_DOWN;
927 mTouchX = x;
928 mTouchY = y;
929 }
930 break;
931 }
932
933 case MotionEvent.ACTION_MOVE: {
934 switch (mTouchMode) {
935 case TOUCH_MODE_IDLE:
936 // Didn't target the thumb, treat normally.
937 break;
938
939 case TOUCH_MODE_DOWN: {
940 final float x = ev.getX();
941 final float y = ev.getY();
942 if (Math.abs(x - mTouchX) > mTouchSlop ||
943 Math.abs(y - mTouchY) > mTouchSlop) {
944 mTouchMode = TOUCH_MODE_DRAGGING;
945 getParent().requestDisallowInterceptTouchEvent(true);
946 mTouchX = x;
947 mTouchY = y;
948 return true;
949 }
950 break;
951 }
952
953 case TOUCH_MODE_DRAGGING: {
954 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700955 final int thumbScrollRange = getThumbScrollRange();
956 final float thumbScrollOffset = x - mTouchX;
957 float dPos;
958 if (thumbScrollRange != 0) {
959 dPos = thumbScrollOffset / thumbScrollRange;
960 } else {
961 // If the thumb scroll range is empty, just use the
962 // movement direction to snap on or off.
963 dPos = thumbScrollOffset > 0 ? 1 : -1;
964 }
965 if (isLayoutRtl()) {
966 dPos = -dPos;
967 }
968 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800969 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800970 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700971 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800972 }
973 return true;
974 }
975 }
976 break;
977 }
978
979 case MotionEvent.ACTION_UP:
980 case MotionEvent.ACTION_CANCEL: {
981 if (mTouchMode == TOUCH_MODE_DRAGGING) {
982 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700983 // Allow super class to handle pressed state, etc.
984 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800985 return true;
986 }
987 mTouchMode = TOUCH_MODE_IDLE;
988 mVelocityTracker.clear();
989 break;
990 }
991 }
992
993 return super.onTouchEvent(ev);
994 }
995
996 private void cancelSuperTouch(MotionEvent ev) {
997 MotionEvent cancel = MotionEvent.obtain(ev);
998 cancel.setAction(MotionEvent.ACTION_CANCEL);
999 super.onTouchEvent(cancel);
1000 cancel.recycle();
1001 }
1002
1003 /**
1004 * Called from onTouchEvent to end a drag operation.
1005 *
1006 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1007 */
1008 private void stopDrag(MotionEvent ev) {
1009 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001010
Alan Viverette86453ff2013-09-26 14:46:08 -07001011 // Commit the change if the event is up and not canceled and the switch
1012 // has not been disabled during the drag.
1013 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001014 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001015 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001016 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001017 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001018 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001019 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001020 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001021 } else {
1022 newState = getTargetCheckedState();
1023 }
Adam Powell12190b32010-11-28 19:07:53 -08001024 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001025 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001026 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001027
Alan Viveretted4e77902014-10-27 17:50:51 -07001028 if (newState != oldState) {
1029 playSoundEffect(SoundEffectConstants.CLICK);
Alan Viveretted4e77902014-10-27 17:50:51 -07001030 }
Alan Viverette03306a02015-08-07 11:34:25 -04001031 // Always call setChecked so that the thumb is moved back to the correct edge
1032 setChecked(newState);
Alan Viverette86453ff2013-09-26 14:46:08 -07001033 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001034 }
1035
1036 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001037 final float targetPosition = newCheckedState ? 1 : 0;
1038 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1039 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1040 mPositionAnimator.setAutoCancel(true);
1041 mPositionAnimator.start();
1042 }
1043
1044 private void cancelPositionAnimator() {
1045 if (mPositionAnimator != null) {
1046 mPositionAnimator.cancel();
1047 }
Adam Powell12190b32010-11-28 19:07:53 -08001048 }
1049
1050 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001051 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001052 }
1053
Alan Viverettecc2688d2013-09-17 17:00:12 -07001054 /**
1055 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1056 *
1057 * @param position new position between [0,1]
1058 */
1059 private void setThumbPosition(float position) {
1060 mThumbPosition = position;
1061 invalidate();
1062 }
1063
1064 @Override
1065 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001066 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001067 }
1068
1069 @Override
1070 public void setChecked(boolean checked) {
1071 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001072
Alan Viverette467d6292014-08-12 15:13:19 -07001073 // Calling the super method may result in setChecked() getting called
1074 // recursively with a different value, so load the REAL value...
1075 checked = isChecked();
1076
Alan Viverette86453ff2013-09-26 14:46:08 -07001077 if (isAttachedToWindow() && isLaidOut()) {
1078 animateThumbToCheckedState(checked);
1079 } else {
1080 // Immediately move the thumb to the new position.
1081 cancelPositionAnimator();
1082 setThumbPosition(checked ? 1 : 0);
1083 }
Adam Powell12190b32010-11-28 19:07:53 -08001084 }
1085
1086 @Override
1087 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1088 super.onLayout(changed, left, top, right, bottom);
1089
Alan Viverette0c0dde72014-07-30 13:29:39 -07001090 int opticalInsetLeft = 0;
1091 int opticalInsetRight = 0;
1092 if (mThumbDrawable != null) {
1093 final Rect trackPadding = mTempRect;
1094 if (mTrackDrawable != null) {
1095 mTrackDrawable.getPadding(trackPadding);
1096 } else {
1097 trackPadding.setEmpty();
1098 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001099
Alan Viverette0c0dde72014-07-30 13:29:39 -07001100 final Insets insets = mThumbDrawable.getOpticalInsets();
1101 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1102 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001103 }
1104
Alan Viverette0c0dde72014-07-30 13:29:39 -07001105 final int switchRight;
1106 final int switchLeft;
1107 if (isLayoutRtl()) {
1108 switchLeft = getPaddingLeft() + opticalInsetLeft;
1109 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1110 } else {
1111 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1112 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1113 }
1114
1115 final int switchTop;
1116 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001117 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1118 default:
1119 case Gravity.TOP:
1120 switchTop = getPaddingTop();
1121 switchBottom = switchTop + mSwitchHeight;
1122 break;
1123
1124 case Gravity.CENTER_VERTICAL:
1125 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1126 mSwitchHeight / 2;
1127 switchBottom = switchTop + mSwitchHeight;
1128 break;
1129
1130 case Gravity.BOTTOM:
1131 switchBottom = getHeight() - getPaddingBottom();
1132 switchTop = switchBottom - mSwitchHeight;
1133 break;
1134 }
1135
1136 mSwitchLeft = switchLeft;
1137 mSwitchTop = switchTop;
1138 mSwitchBottom = switchBottom;
1139 mSwitchRight = switchRight;
1140 }
1141
1142 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001143 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001144 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001145 final int switchLeft = mSwitchLeft;
1146 final int switchTop = mSwitchTop;
1147 final int switchRight = mSwitchRight;
1148 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001149
1150 int thumbInitialLeft = switchLeft + getThumbOffset();
1151
1152 final Insets thumbInsets;
1153 if (mThumbDrawable != null) {
1154 thumbInsets = mThumbDrawable.getOpticalInsets();
1155 } else {
1156 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001157 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001158
Alan Viverette0c0dde72014-07-30 13:29:39 -07001159 // Layout the track.
1160 if (mTrackDrawable != null) {
1161 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001162
Alan Viverette0c0dde72014-07-30 13:29:39 -07001163 // Adjust thumb position for track padding.
1164 thumbInitialLeft += padding.left;
1165
1166 // If necessary, offset by the optical insets of the thumb asset.
1167 int trackLeft = switchLeft;
1168 int trackTop = switchTop;
1169 int trackRight = switchRight;
1170 int trackBottom = switchBottom;
1171 if (thumbInsets != Insets.NONE) {
1172 if (thumbInsets.left > padding.left) {
1173 trackLeft += thumbInsets.left - padding.left;
1174 }
1175 if (thumbInsets.top > padding.top) {
1176 trackTop += thumbInsets.top - padding.top;
1177 }
1178 if (thumbInsets.right > padding.right) {
1179 trackRight -= thumbInsets.right - padding.right;
1180 }
1181 if (thumbInsets.bottom > padding.bottom) {
1182 trackBottom -= thumbInsets.bottom - padding.bottom;
1183 }
1184 }
1185 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1186 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001187
Alan Viverette661e6362014-05-12 10:55:37 -07001188 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001189 if (mThumbDrawable != null) {
1190 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001191
1192 final int thumbLeft = thumbInitialLeft - padding.left;
1193 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001194 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001195
Alan Viverette4d065a02014-07-11 15:28:38 -07001196 final Drawable background = getBackground();
1197 if (background != null) {
1198 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1199 }
Alan Viverette61956602014-04-22 19:07:06 -07001200 }
1201
Alan Viverettead2f8e32014-05-16 13:28:33 -07001202 // Draw the background.
1203 super.draw(c);
1204 }
1205
1206 @Override
1207 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001208 super.onDraw(canvas);
1209
Alan Viverette4d065a02014-07-11 15:28:38 -07001210 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001211 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001212 if (trackDrawable != null) {
1213 trackDrawable.getPadding(padding);
1214 } else {
1215 padding.setEmpty();
1216 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001217
1218 final int switchTop = mSwitchTop;
1219 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001220 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001221 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001222
Alan Viverette4d065a02014-07-11 15:28:38 -07001223 final Drawable thumbDrawable = mThumbDrawable;
1224 if (trackDrawable != null) {
1225 if (mSplitTrack && thumbDrawable != null) {
1226 final Insets insets = thumbDrawable.getOpticalInsets();
1227 thumbDrawable.copyBounds(padding);
1228 padding.left += insets.left;
1229 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001230
Alan Viverette4d065a02014-07-11 15:28:38 -07001231 final int saveCount = canvas.save();
1232 canvas.clipRect(padding, Op.DIFFERENCE);
1233 trackDrawable.draw(canvas);
1234 canvas.restoreToCount(saveCount);
1235 } else {
1236 trackDrawable.draw(canvas);
1237 }
Alan Viverette661e6362014-05-12 10:55:37 -07001238 }
Alan Viverette61956602014-04-22 19:07:06 -07001239
1240 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001241
1242 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001243 thumbDrawable.draw(canvas);
1244 }
Adam Powell12190b32010-11-28 19:07:53 -08001245
Alan Viverette5876ff42014-03-03 17:40:46 -08001246 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001247 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001248 final int drawableState[] = getDrawableState();
1249 if (mTextColors != null) {
1250 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1251 }
1252 mTextPaint.drawableState = drawableState;
1253
Alan Viverette4d065a02014-07-11 15:28:38 -07001254 final int cX;
1255 if (thumbDrawable != null) {
1256 final Rect bounds = thumbDrawable.getBounds();
1257 cX = bounds.left + bounds.right;
1258 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001259 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001260 }
1261
1262 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001263 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1264 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001265 switchText.draw(canvas);
1266 }
Adam Powell12190b32010-11-28 19:07:53 -08001267
Alan Viverette5876ff42014-03-03 17:40:46 -08001268 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001269 }
1270
1271 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001272 public int getCompoundPaddingLeft() {
1273 if (!isLayoutRtl()) {
1274 return super.getCompoundPaddingLeft();
1275 }
1276 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1277 if (!TextUtils.isEmpty(getText())) {
1278 padding += mSwitchPadding;
1279 }
1280 return padding;
1281 }
1282
1283 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001284 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001285 if (isLayoutRtl()) {
1286 return super.getCompoundPaddingRight();
1287 }
Adam Powell12190b32010-11-28 19:07:53 -08001288 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1289 if (!TextUtils.isEmpty(getText())) {
1290 padding += mSwitchPadding;
1291 }
1292 return padding;
1293 }
1294
Alan Viverettecc2688d2013-09-17 17:00:12 -07001295 /**
1296 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001297 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001298 *
1299 * @return thumb offset
1300 */
1301 private int getThumbOffset() {
1302 final float thumbPosition;
1303 if (isLayoutRtl()) {
1304 thumbPosition = 1 - mThumbPosition;
1305 } else {
1306 thumbPosition = mThumbPosition;
1307 }
1308 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1309 }
1310
Adam Powell12190b32010-11-28 19:07:53 -08001311 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001312 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001313 final Rect padding = mTempRect;
1314 mTrackDrawable.getPadding(padding);
1315
1316 final Insets insets;
1317 if (mThumbDrawable != null) {
1318 insets = mThumbDrawable.getOpticalInsets();
1319 } else {
1320 insets = Insets.NONE;
1321 }
1322
1323 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1324 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001325 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001326 return 0;
1327 }
Adam Powell12190b32010-11-28 19:07:53 -08001328 }
1329
1330 @Override
1331 protected int[] onCreateDrawableState(int extraSpace) {
1332 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1333 if (isChecked()) {
1334 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1335 }
1336 return drawableState;
1337 }
1338
1339 @Override
1340 protected void drawableStateChanged() {
1341 super.drawableStateChanged();
1342
Alan Viverettead0020f2015-09-04 10:10:42 -04001343 final int[] state = getDrawableState();
1344 boolean changed = false;
Adam Powell12190b32010-11-28 19:07:53 -08001345
Alan Viverettead0020f2015-09-04 10:10:42 -04001346 final Drawable thumbDrawable = mThumbDrawable;
1347 if (thumbDrawable != null && thumbDrawable.isStateful()) {
1348 changed |= thumbDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001349 }
1350
Alan Viverettead0020f2015-09-04 10:10:42 -04001351 final Drawable trackDrawable = mTrackDrawable;
1352 if (trackDrawable != null && trackDrawable.isStateful()) {
1353 changed |= trackDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001354 }
Adam Powell12190b32010-11-28 19:07:53 -08001355
Alan Viverettead0020f2015-09-04 10:10:42 -04001356 if (changed) {
1357 invalidate();
1358 }
Adam Powell12190b32010-11-28 19:07:53 -08001359 }
1360
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001361 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001362 public void drawableHotspotChanged(float x, float y) {
1363 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001364
1365 if (mThumbDrawable != null) {
1366 mThumbDrawable.setHotspot(x, y);
1367 }
1368
1369 if (mTrackDrawable != null) {
1370 mTrackDrawable.setHotspot(x, y);
1371 }
1372 }
1373
Adam Powell12190b32010-11-28 19:07:53 -08001374 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -05001375 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell12190b32010-11-28 19:07:53 -08001376 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1377 }
1378
1379 @Override
1380 public void jumpDrawablesToCurrentState() {
1381 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001382
1383 if (mThumbDrawable != null) {
1384 mThumbDrawable.jumpToCurrentState();
1385 }
1386
1387 if (mTrackDrawable != null) {
1388 mTrackDrawable.jumpToCurrentState();
1389 }
1390
Alan Viveretteee20d772015-09-23 14:23:21 -04001391 if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001392 mPositionAnimator.end();
1393 mPositionAnimator = null;
1394 }
Adam Powell12190b32010-11-28 19:07:53 -08001395 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001396
1397 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001398 public CharSequence getAccessibilityClassName() {
1399 return Switch.class.getName();
1400 }
1401
1402 @Override
Dianne Hackborn49b043f2015-05-07 14:21:38 -07001403 public void onProvideStructure(ViewStructure structure) {
1404 super.onProvideStructure(structure);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001405 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1406 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001407 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001408 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001409 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001410 } else {
1411 StringBuilder newText = new StringBuilder();
1412 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001413 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001414 }
James Cook5cfaae42015-05-28 15:52:44 -07001415 // The style of the label text is provided via the base TextView class. This is more
1416 // relevant than the style of the (optional) on/off text on the switch button itself,
1417 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001418 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001419 }
1420
Alan Viverettea54956a2015-01-07 16:05:02 -08001421 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001422 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001423 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1424 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001425 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1426 if (!TextUtils.isEmpty(switchText)) {
1427 CharSequence oldText = info.getText();
1428 if (TextUtils.isEmpty(oldText)) {
1429 info.setText(switchText);
1430 } else {
1431 StringBuilder newText = new StringBuilder();
1432 newText.append(oldText).append(' ').append(switchText);
1433 info.setText(newText);
1434 }
1435 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001436 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001437
1438 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1439 @Override
1440 public Float get(Switch object) {
1441 return object.mThumbPosition;
1442 }
1443
1444 @Override
1445 public void setValue(Switch object, float value) {
1446 object.setThumbPosition(value);
1447 }
1448 };
Adam Powell12190b32010-11-28 19:07:53 -08001449}