blob: a01d65a1df202806d99389d3f598fb03f90c8455 [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 *
Mark Lu34040322016-08-30 18:12:30 -070068 * <p>{@link android.support.v7.widget.SwitchCompat} is a version of
69 * the Switch widget which runs on devices back to API 7.</p>
70 *
Scott Main4c359b72012-07-24 15:51:27 -070071 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
72 * guide.</p>
73 *
74 * @attr ref android.R.styleable#Switch_textOn
75 * @attr ref android.R.styleable#Switch_textOff
76 * @attr ref android.R.styleable#Switch_switchMinWidth
77 * @attr ref android.R.styleable#Switch_switchPadding
78 * @attr ref android.R.styleable#Switch_switchTextAppearance
79 * @attr ref android.R.styleable#Switch_thumb
80 * @attr ref android.R.styleable#Switch_thumbTextPadding
81 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080082 */
83public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070084 private static final int THUMB_ANIMATION_DURATION = 250;
85
Adam Powell12190b32010-11-28 19:07:53 -080086 private static final int TOUCH_MODE_IDLE = 0;
87 private static final int TOUCH_MODE_DOWN = 1;
88 private static final int TOUCH_MODE_DRAGGING = 2;
89
90 // Enum for the "typeface" XML parameter.
91 private static final int SANS = 1;
92 private static final int SERIF = 2;
93 private static final int MONOSPACE = 3;
94
95 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080096 private ColorStateList mThumbTintList = null;
97 private PorterDuff.Mode mThumbTintMode = null;
98 private boolean mHasThumbTint = false;
99 private boolean mHasThumbTintMode = false;
100
Adam Powell12190b32010-11-28 19:07:53 -0800101 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -0800102 private ColorStateList mTrackTintList = null;
103 private PorterDuff.Mode mTrackTintMode = null;
104 private boolean mHasTrackTint = false;
105 private boolean mHasTrackTintMode = false;
106
Adam Powell12190b32010-11-28 19:07:53 -0800107 private int mThumbTextPadding;
108 private int mSwitchMinWidth;
109 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700110 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800111 private CharSequence mTextOn;
112 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700113 private boolean mShowText;
Adam Powell12190b32010-11-28 19:07:53 -0800114
115 private int mTouchMode;
116 private int mTouchSlop;
117 private float mTouchX;
118 private float mTouchY;
119 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
120 private int mMinFlingVelocity;
121
122 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700123
Alan Viverette0c0dde72014-07-30 13:29:39 -0700124 /**
125 * Width required to draw the switch track and thumb. Includes padding and
126 * optical bounds for both the track and thumb.
127 */
128 private int mSwitchWidth;
129
130 /**
131 * Height required to draw the switch track and thumb. Includes padding and
132 * optical bounds for both the track and thumb.
133 */
134 private int mSwitchHeight;
135
136 /**
137 * Width of the thumb's content region. Does not include padding or
138 * optical bounds.
139 */
140 private int mThumbWidth;
141
142 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800143 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700144
145 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800146 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700147
148 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800149 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700150
151 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800152 private int mSwitchBottom;
153
154 private TextPaint mTextPaint;
155 private ColorStateList mTextColors;
156 private Layout mOnLayout;
157 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400158 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700159 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800160
Adam Powellbe0a4532010-11-29 17:47:48 -0800161 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800162 private final Rect mTempRect = new Rect();
163
164 private static final int[] CHECKED_STATE_SET = {
165 R.attr.state_checked
166 };
167
168 /**
169 * Construct a new Switch with default styling.
170 *
171 * @param context The Context that will determine this widget's theming.
172 */
173 public Switch(Context context) {
174 this(context, null);
175 }
176
177 /**
178 * Construct a new Switch with default styling, overriding specific style
179 * attributes as requested.
180 *
181 * @param context The Context that will determine this widget's theming.
182 * @param attrs Specification of attributes that should deviate from default styling.
183 */
184 public Switch(Context context, AttributeSet attrs) {
185 this(context, attrs, com.android.internal.R.attr.switchStyle);
186 }
187
188 /**
189 * Construct a new Switch with a default style determined by the given theme attribute,
190 * overriding specific style attributes as requested.
191 *
192 * @param context The Context that will determine this widget's theming.
193 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700194 * @param defStyleAttr An attribute in the current theme that contains a
195 * reference to a style resource that supplies default values for
196 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800197 */
Alan Viverette617feb92013-09-09 18:09:13 -0700198 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
199 this(context, attrs, defStyleAttr, 0);
200 }
201
202
203 /**
204 * Construct a new Switch with a default style determined by the given theme
205 * attribute or style resource, overriding specific style attributes as
206 * requested.
207 *
208 * @param context The Context that will determine this widget's theming.
209 * @param attrs Specification of attributes that should deviate from the
210 * default styling.
211 * @param defStyleAttr An attribute in the current theme that contains a
212 * reference to a style resource that supplies default values for
213 * the view. Can be 0 to not look for defaults.
214 * @param defStyleRes A resource identifier of a style resource that
215 * supplies default values for the view, used only if
216 * defStyleAttr is 0 or can not be found in the theme. Can be 0
217 * to not look for defaults.
218 */
219 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
220 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800221
Chris Craik6a49dde2015-05-12 10:28:14 -0700222 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700223
224 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800225 mTextPaint.density = res.getDisplayMetrics().density;
226 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
227
Alan Viverette617feb92013-09-09 18:09:13 -0700228 final TypedArray a = context.obtainStyledAttributes(
229 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700230 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700231 if (mThumbDrawable != null) {
232 mThumbDrawable.setCallback(this);
233 }
Chet Haase150176d2011-08-26 09:54:06 -0700234 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700235 if (mTrackDrawable != null) {
236 mTrackDrawable.setCallback(this);
237 }
Adam Powell12190b32010-11-28 19:07:53 -0800238 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
239 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700240 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800241 mThumbTextPadding = a.getDimensionPixelSize(
242 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
243 mSwitchMinWidth = a.getDimensionPixelSize(
244 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
245 mSwitchPadding = a.getDimensionPixelSize(
246 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700247 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800248
Jun Mukai9fb302c2015-06-24 18:31:06 -0700249 ColorStateList thumbTintList = a.getColorStateList(
250 com.android.internal.R.styleable.Switch_thumbTint);
251 if (thumbTintList != null) {
252 mThumbTintList = thumbTintList;
253 mHasThumbTint = true;
254 }
255 PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
256 a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
257 if (mThumbTintMode != thumbTintMode) {
258 mThumbTintMode = thumbTintMode;
259 mHasThumbTintMode = true;
260 }
261 if (mHasThumbTint || mHasThumbTintMode) {
262 applyThumbTint();
263 }
264
265 ColorStateList trackTintList = a.getColorStateList(
266 com.android.internal.R.styleable.Switch_trackTint);
267 if (trackTintList != null) {
268 mTrackTintList = trackTintList;
269 mHasTrackTint = true;
270 }
271 PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
272 a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
273 if (mTrackTintMode != trackTintMode) {
274 mTrackTintMode = trackTintMode;
275 mHasTrackTintMode = true;
276 }
277 if (mHasTrackTint || mHasTrackTintMode) {
278 applyTrackTint();
279 }
280
Alan Viverette661e6362014-05-12 10:55:37 -0700281 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800282 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
283 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700284 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800285 }
286 a.recycle();
287
Alan Viverette661e6362014-05-12 10:55:37 -0700288 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800289 mTouchSlop = config.getScaledTouchSlop();
290 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
291
292 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700293 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800294 setChecked(isChecked());
295 }
296
297 /**
298 * Sets the switch text color, size, style, hint color, and highlight color
299 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800300 *
301 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800302 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700303 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800304 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700305 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800306 com.android.internal.R.styleable.TextAppearance);
307
308 ColorStateList colors;
309 int ts;
310
311 colors = appearance.getColorStateList(com.android.internal.R.styleable.
312 TextAppearance_textColor);
313 if (colors != null) {
314 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700315 } else {
316 // If no color set in TextAppearance, default to the view's textColor
317 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800318 }
319
320 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
321 TextAppearance_textSize, 0);
322 if (ts != 0) {
323 if (ts != mTextPaint.getTextSize()) {
324 mTextPaint.setTextSize(ts);
325 requestLayout();
326 }
327 }
328
329 int typefaceIndex, styleIndex;
330
331 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
332 TextAppearance_typeface, -1);
333 styleIndex = appearance.getInt(com.android.internal.R.styleable.
334 TextAppearance_textStyle, -1);
335
336 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
337
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400338 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
339 TextAppearance_textAllCaps, false);
340 if (allCaps) {
341 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
342 mSwitchTransformationMethod.setLengthChangesAllowed(true);
343 } else {
344 mSwitchTransformationMethod = null;
345 }
346
Adam Powell12190b32010-11-28 19:07:53 -0800347 appearance.recycle();
348 }
349
350 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
351 Typeface tf = null;
352 switch (typefaceIndex) {
353 case SANS:
354 tf = Typeface.SANS_SERIF;
355 break;
356
357 case SERIF:
358 tf = Typeface.SERIF;
359 break;
360
361 case MONOSPACE:
362 tf = Typeface.MONOSPACE;
363 break;
364 }
365
366 setSwitchTypeface(tf, styleIndex);
367 }
368
369 /**
370 * Sets the typeface and style in which the text should be displayed on the
371 * switch, and turns on the fake bold and italic bits in the Paint if the
372 * Typeface that you provided does not have all the bits in the
373 * style that you specified.
374 */
375 public void setSwitchTypeface(Typeface tf, int style) {
376 if (style > 0) {
377 if (tf == null) {
378 tf = Typeface.defaultFromStyle(style);
379 } else {
380 tf = Typeface.create(tf, style);
381 }
382
383 setSwitchTypeface(tf);
384 // now compute what (if any) algorithmic styling is needed
385 int typefaceStyle = tf != null ? tf.getStyle() : 0;
386 int need = style & ~typefaceStyle;
387 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
388 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
389 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700390 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800391 mTextPaint.setTextSkewX(0);
392 setSwitchTypeface(tf);
393 }
394 }
395
396 /**
Chet Haase150176d2011-08-26 09:54:06 -0700397 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800398 * Note that not all Typeface families actually have bold and italic
399 * variants, so you may need to use
400 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
401 * that you actually want.
402 *
403 * @attr ref android.R.styleable#TextView_typeface
404 * @attr ref android.R.styleable#TextView_textStyle
405 */
406 public void setSwitchTypeface(Typeface tf) {
407 if (mTextPaint.getTypeface() != tf) {
408 mTextPaint.setTypeface(tf);
409
410 requestLayout();
411 invalidate();
412 }
413 }
414
415 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800416 * Set the amount of horizontal padding between the switch and the associated text.
417 *
418 * @param pixels Amount of padding in pixels
419 *
420 * @attr ref android.R.styleable#Switch_switchPadding
421 */
422 public void setSwitchPadding(int pixels) {
423 mSwitchPadding = pixels;
424 requestLayout();
425 }
426
427 /**
428 * Get the amount of horizontal padding between the switch and the associated text.
429 *
430 * @return Amount of padding in pixels
431 *
432 * @attr ref android.R.styleable#Switch_switchPadding
433 */
434 public int getSwitchPadding() {
435 return mSwitchPadding;
436 }
437
438 /**
439 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
440 * of this value and its measured width as determined by the switch drawables and text used.
441 *
442 * @param pixels Minimum width of the switch in pixels
443 *
444 * @attr ref android.R.styleable#Switch_switchMinWidth
445 */
446 public void setSwitchMinWidth(int pixels) {
447 mSwitchMinWidth = pixels;
448 requestLayout();
449 }
450
451 /**
452 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
453 * of this value and its measured width as determined by the switch drawables and text used.
454 *
455 * @return Minimum width of the switch in pixels
456 *
457 * @attr ref android.R.styleable#Switch_switchMinWidth
458 */
459 public int getSwitchMinWidth() {
460 return mSwitchMinWidth;
461 }
462
463 /**
464 * Set the horizontal padding around the text drawn on the switch itself.
465 *
466 * @param pixels Horizontal padding for switch thumb text in pixels
467 *
468 * @attr ref android.R.styleable#Switch_thumbTextPadding
469 */
470 public void setThumbTextPadding(int pixels) {
471 mThumbTextPadding = pixels;
472 requestLayout();
473 }
474
475 /**
476 * Get the horizontal padding around the text drawn on the switch itself.
477 *
478 * @return Horizontal padding for switch thumb text in pixels
479 *
480 * @attr ref android.R.styleable#Switch_thumbTextPadding
481 */
482 public int getThumbTextPadding() {
483 return mThumbTextPadding;
484 }
485
486 /**
487 * Set the drawable used for the track that the switch slides within.
488 *
489 * @param track Track drawable
490 *
491 * @attr ref android.R.styleable#Switch_track
492 */
493 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700494 if (mTrackDrawable != null) {
495 mTrackDrawable.setCallback(null);
496 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800497 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700498 if (track != null) {
499 track.setCallback(this);
500 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800501 requestLayout();
502 }
503
504 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800505 * Set the drawable used for the track that the switch slides within.
506 *
Adam Powelldca510e2012-03-08 20:06:39 -0800507 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800508 *
509 * @attr ref android.R.styleable#Switch_track
510 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700511 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800512 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800513 }
514
515 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800516 * Get the drawable used for the track that the switch slides within.
517 *
518 * @return Track drawable
519 *
520 * @attr ref android.R.styleable#Switch_track
521 */
522 public Drawable getTrackDrawable() {
523 return mTrackDrawable;
524 }
525
526 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800527 * Applies a tint to the track drawable. Does not modify the current
528 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
529 * <p>
530 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
531 * automatically mutate the drawable and apply the specified tint and tint
532 * mode using {@link Drawable#setTintList(ColorStateList)}.
533 *
534 * @param tint the tint to apply, may be {@code null} to clear tint
535 *
536 * @attr ref android.R.styleable#Switch_trackTint
537 * @see #getTrackTintList()
538 * @see Drawable#setTintList(ColorStateList)
539 */
540 public void setTrackTintList(@Nullable ColorStateList tint) {
541 mTrackTintList = tint;
542 mHasTrackTint = true;
543
544 applyTrackTint();
545 }
546
547 /**
548 * @return the tint applied to the track drawable
549 * @attr ref android.R.styleable#Switch_trackTint
550 * @see #setTrackTintList(ColorStateList)
551 */
552 @Nullable
553 public ColorStateList getTrackTintList() {
554 return mTrackTintList;
555 }
556
557 /**
558 * Specifies the blending mode used to apply the tint specified by
559 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
560 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
561 *
562 * @param tintMode the blending mode used to apply the tint, may be
563 * {@code null} to clear tint
564 * @attr ref android.R.styleable#Switch_trackTintMode
565 * @see #getTrackTintMode()
566 * @see Drawable#setTintMode(PorterDuff.Mode)
567 */
568 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
569 mTrackTintMode = tintMode;
570 mHasTrackTintMode = true;
571
572 applyTrackTint();
573 }
574
575 /**
576 * @return the blending mode used to apply the tint to the track
577 * drawable
578 * @attr ref android.R.styleable#Switch_trackTintMode
579 * @see #setTrackTintMode(PorterDuff.Mode)
580 */
581 @Nullable
582 public PorterDuff.Mode getTrackTintMode() {
583 return mTrackTintMode;
584 }
585
586 private void applyTrackTint() {
587 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
588 mTrackDrawable = mTrackDrawable.mutate();
589
590 if (mHasTrackTint) {
591 mTrackDrawable.setTintList(mTrackTintList);
592 }
593
594 if (mHasTrackTintMode) {
595 mTrackDrawable.setTintMode(mTrackTintMode);
596 }
597
598 // The drawable (or one of its children) may not have been
599 // stateful before applying the tint, so let's try again.
600 if (mTrackDrawable.isStateful()) {
601 mTrackDrawable.setState(getDrawableState());
602 }
603 }
604 }
605
606 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800607 * Set the drawable used for the switch "thumb" - the piece that the user
608 * can physically touch and drag along the track.
609 *
610 * @param thumb Thumb drawable
611 *
612 * @attr ref android.R.styleable#Switch_thumb
613 */
614 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700615 if (mThumbDrawable != null) {
616 mThumbDrawable.setCallback(null);
617 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800618 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700619 if (thumb != null) {
620 thumb.setCallback(this);
621 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800622 requestLayout();
623 }
624
625 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800626 * Set the drawable used for the switch "thumb" - the piece that the user
627 * can physically touch and drag along the track.
628 *
Adam Powelldca510e2012-03-08 20:06:39 -0800629 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800630 *
631 * @attr ref android.R.styleable#Switch_thumb
632 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700633 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800634 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800635 }
636
637 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800638 * Get the drawable used for the switch "thumb" - the piece that the user
639 * can physically touch and drag along the track.
640 *
641 * @return Thumb drawable
642 *
643 * @attr ref android.R.styleable#Switch_thumb
644 */
645 public Drawable getThumbDrawable() {
646 return mThumbDrawable;
647 }
648
649 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800650 * Applies a tint to the thumb drawable. Does not modify the current
651 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
652 * <p>
653 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
654 * automatically mutate the drawable and apply the specified tint and tint
655 * mode using {@link Drawable#setTintList(ColorStateList)}.
656 *
657 * @param tint the tint to apply, may be {@code null} to clear tint
658 *
659 * @attr ref android.R.styleable#Switch_thumbTint
660 * @see #getThumbTintList()
661 * @see Drawable#setTintList(ColorStateList)
662 */
663 public void setThumbTintList(@Nullable ColorStateList tint) {
664 mThumbTintList = tint;
665 mHasThumbTint = true;
666
667 applyThumbTint();
668 }
669
670 /**
671 * @return the tint applied to the thumb drawable
672 * @attr ref android.R.styleable#Switch_thumbTint
673 * @see #setThumbTintList(ColorStateList)
674 */
675 @Nullable
676 public ColorStateList getThumbTintList() {
677 return mThumbTintList;
678 }
679
680 /**
681 * Specifies the blending mode used to apply the tint specified by
682 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
683 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
684 *
685 * @param tintMode the blending mode used to apply the tint, may be
686 * {@code null} to clear tint
687 * @attr ref android.R.styleable#Switch_thumbTintMode
688 * @see #getThumbTintMode()
689 * @see Drawable#setTintMode(PorterDuff.Mode)
690 */
691 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
692 mThumbTintMode = tintMode;
693 mHasThumbTintMode = true;
694
695 applyThumbTint();
696 }
697
698 /**
699 * @return the blending mode used to apply the tint to the thumb
700 * drawable
701 * @attr ref android.R.styleable#Switch_thumbTintMode
702 * @see #setThumbTintMode(PorterDuff.Mode)
703 */
704 @Nullable
705 public PorterDuff.Mode getThumbTintMode() {
706 return mThumbTintMode;
707 }
708
709 private void applyThumbTint() {
710 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
711 mThumbDrawable = mThumbDrawable.mutate();
712
713 if (mHasThumbTint) {
714 mThumbDrawable.setTintList(mThumbTintList);
715 }
716
717 if (mHasThumbTintMode) {
718 mThumbDrawable.setTintMode(mThumbTintMode);
719 }
720
721 // The drawable (or one of its children) may not have been
722 // stateful before applying the tint, so let's try again.
723 if (mThumbDrawable.isStateful()) {
724 mThumbDrawable.setState(getDrawableState());
725 }
726 }
727 }
728
729 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700730 * Specifies whether the track should be split by the thumb. When true,
731 * the thumb's optical bounds will be clipped out of the track drawable,
732 * then the thumb will be drawn into the resulting gap.
733 *
734 * @param splitTrack Whether the track should be split by the thumb
735 *
736 * @attr ref android.R.styleable#Switch_splitTrack
737 */
738 public void setSplitTrack(boolean splitTrack) {
739 mSplitTrack = splitTrack;
740 invalidate();
741 }
742
743 /**
744 * Returns whether the track should be split by the thumb.
745 *
746 * @attr ref android.R.styleable#Switch_splitTrack
747 */
748 public boolean getSplitTrack() {
749 return mSplitTrack;
750 }
751
752 /**
Chet Haase150176d2011-08-26 09:54:06 -0700753 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800754 *
755 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800756 */
757 public CharSequence getTextOn() {
758 return mTextOn;
759 }
760
761 /**
Chet Haase150176d2011-08-26 09:54:06 -0700762 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800763 *
764 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800765 */
766 public void setTextOn(CharSequence textOn) {
767 mTextOn = textOn;
768 requestLayout();
769 }
770
771 /**
Chet Haase150176d2011-08-26 09:54:06 -0700772 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800773 *
774 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800775 */
776 public CharSequence getTextOff() {
777 return mTextOff;
778 }
779
780 /**
Chet Haase150176d2011-08-26 09:54:06 -0700781 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800782 *
783 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800784 */
785 public void setTextOff(CharSequence textOff) {
786 mTextOff = textOff;
787 requestLayout();
788 }
789
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700790 /**
791 * Sets whether the on/off text should be displayed.
792 *
793 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700794 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700795 */
796 public void setShowText(boolean showText) {
797 if (mShowText != showText) {
798 mShowText = showText;
799 requestLayout();
800 }
801 }
802
803 /**
804 * @return whether the on/off text should be displayed
Alan Viverette0c0dde72014-07-30 13:29:39 -0700805 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700806 */
807 public boolean getShowText() {
808 return mShowText;
809 }
810
Adam Powell12190b32010-11-28 19:07:53 -0800811 @Override
812 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700813 if (mShowText) {
814 if (mOnLayout == null) {
815 mOnLayout = makeLayout(mTextOn);
816 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800817
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700818 if (mOffLayout == null) {
819 mOffLayout = makeLayout(mTextOff);
820 }
Adam Powell12190b32010-11-28 19:07:53 -0800821 }
822
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000823 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700824 final int thumbWidth;
825 final int thumbHeight;
826 if (mThumbDrawable != null) {
827 // Cached thumb width does not include padding.
828 mThumbDrawable.getPadding(padding);
829 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
830 thumbHeight = mThumbDrawable.getIntrinsicHeight();
831 } else {
832 thumbWidth = 0;
833 thumbHeight = 0;
834 }
835
836 final int maxTextWidth;
837 if (mShowText) {
838 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
839 + mThumbTextPadding * 2;
840 } else {
841 maxTextWidth = 0;
842 }
843
844 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
845
846 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700847 if (mTrackDrawable != null) {
848 mTrackDrawable.getPadding(padding);
849 trackHeight = mTrackDrawable.getIntrinsicHeight();
850 } else {
851 padding.setEmpty();
852 trackHeight = 0;
853 }
854
Alan Viverette0c0dde72014-07-30 13:29:39 -0700855 // Adjust left and right padding to ensure there's enough room for the
856 // thumb's padding (when present).
857 int paddingLeft = padding.left;
858 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700859 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700860 final Insets inset = mThumbDrawable.getOpticalInsets();
861 paddingLeft = Math.max(paddingLeft, inset.left);
862 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700863 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800864
Adam Powell12190b32010-11-28 19:07:53 -0800865 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700866 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700867 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800868 mSwitchWidth = switchWidth;
869 mSwitchHeight = switchHeight;
870
871 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700872
Adam Powell12190b32010-11-28 19:07:53 -0800873 final int measuredHeight = getMeasuredHeight();
874 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800875 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800876 }
877 }
878
Alan Viverettea54956a2015-01-07 16:05:02 -0800879 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700880 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800881 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
882 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700883
884 final CharSequence text = isChecked() ? mTextOn : mTextOff;
885 if (text != null) {
886 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700887 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700888 }
889
Adam Powell12190b32010-11-28 19:07:53 -0800890 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400891 final CharSequence transformed = (mSwitchTransformationMethod != null)
892 ? mSwitchTransformationMethod.getTransformation(text, this)
893 : text;
894
895 return new StaticLayout(transformed, mTextPaint,
896 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800897 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
898 }
899
900 /**
901 * @return true if (x, y) is within the target area of the switch thumb
902 */
903 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800904 if (mThumbDrawable == null) {
905 return false;
906 }
907
Alan Viverettecc2688d2013-09-17 17:00:12 -0700908 // Relies on mTempRect, MUST be called first!
909 final int thumbOffset = getThumbOffset();
910
Adam Powell12190b32010-11-28 19:07:53 -0800911 mThumbDrawable.getPadding(mTempRect);
912 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700913 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800914 final int thumbRight = thumbLeft + mThumbWidth +
915 mTempRect.left + mTempRect.right + mTouchSlop;
916 final int thumbBottom = mSwitchBottom + mTouchSlop;
917 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
918 }
919
920 @Override
921 public boolean onTouchEvent(MotionEvent ev) {
922 mVelocityTracker.addMovement(ev);
923 final int action = ev.getActionMasked();
924 switch (action) {
925 case MotionEvent.ACTION_DOWN: {
926 final float x = ev.getX();
927 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700928 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800929 mTouchMode = TOUCH_MODE_DOWN;
930 mTouchX = x;
931 mTouchY = y;
932 }
933 break;
934 }
935
936 case MotionEvent.ACTION_MOVE: {
937 switch (mTouchMode) {
938 case TOUCH_MODE_IDLE:
939 // Didn't target the thumb, treat normally.
940 break;
941
942 case TOUCH_MODE_DOWN: {
943 final float x = ev.getX();
944 final float y = ev.getY();
945 if (Math.abs(x - mTouchX) > mTouchSlop ||
946 Math.abs(y - mTouchY) > mTouchSlop) {
947 mTouchMode = TOUCH_MODE_DRAGGING;
948 getParent().requestDisallowInterceptTouchEvent(true);
949 mTouchX = x;
950 mTouchY = y;
951 return true;
952 }
953 break;
954 }
955
956 case TOUCH_MODE_DRAGGING: {
957 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700958 final int thumbScrollRange = getThumbScrollRange();
959 final float thumbScrollOffset = x - mTouchX;
960 float dPos;
961 if (thumbScrollRange != 0) {
962 dPos = thumbScrollOffset / thumbScrollRange;
963 } else {
964 // If the thumb scroll range is empty, just use the
965 // movement direction to snap on or off.
966 dPos = thumbScrollOffset > 0 ? 1 : -1;
967 }
968 if (isLayoutRtl()) {
969 dPos = -dPos;
970 }
971 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800972 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800973 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700974 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800975 }
976 return true;
977 }
978 }
979 break;
980 }
981
982 case MotionEvent.ACTION_UP:
983 case MotionEvent.ACTION_CANCEL: {
984 if (mTouchMode == TOUCH_MODE_DRAGGING) {
985 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700986 // Allow super class to handle pressed state, etc.
987 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800988 return true;
989 }
990 mTouchMode = TOUCH_MODE_IDLE;
991 mVelocityTracker.clear();
992 break;
993 }
994 }
995
996 return super.onTouchEvent(ev);
997 }
998
999 private void cancelSuperTouch(MotionEvent ev) {
1000 MotionEvent cancel = MotionEvent.obtain(ev);
1001 cancel.setAction(MotionEvent.ACTION_CANCEL);
1002 super.onTouchEvent(cancel);
1003 cancel.recycle();
1004 }
1005
1006 /**
1007 * Called from onTouchEvent to end a drag operation.
1008 *
1009 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1010 */
1011 private void stopDrag(MotionEvent ev) {
1012 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001013
Alan Viverette86453ff2013-09-26 14:46:08 -07001014 // Commit the change if the event is up and not canceled and the switch
1015 // has not been disabled during the drag.
1016 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001017 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001018 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001019 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001020 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001021 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001022 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001023 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001024 } else {
1025 newState = getTargetCheckedState();
1026 }
Adam Powell12190b32010-11-28 19:07:53 -08001027 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001028 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001029 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001030
Alan Viveretted4e77902014-10-27 17:50:51 -07001031 if (newState != oldState) {
1032 playSoundEffect(SoundEffectConstants.CLICK);
Alan Viveretted4e77902014-10-27 17:50:51 -07001033 }
Alan Viverette03306a02015-08-07 11:34:25 -04001034 // Always call setChecked so that the thumb is moved back to the correct edge
1035 setChecked(newState);
Alan Viverette86453ff2013-09-26 14:46:08 -07001036 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001037 }
1038
1039 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001040 final float targetPosition = newCheckedState ? 1 : 0;
1041 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1042 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1043 mPositionAnimator.setAutoCancel(true);
1044 mPositionAnimator.start();
1045 }
1046
1047 private void cancelPositionAnimator() {
1048 if (mPositionAnimator != null) {
1049 mPositionAnimator.cancel();
1050 }
Adam Powell12190b32010-11-28 19:07:53 -08001051 }
1052
1053 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001054 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001055 }
1056
Alan Viverettecc2688d2013-09-17 17:00:12 -07001057 /**
1058 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1059 *
1060 * @param position new position between [0,1]
1061 */
1062 private void setThumbPosition(float position) {
1063 mThumbPosition = position;
1064 invalidate();
1065 }
1066
1067 @Override
1068 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001069 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001070 }
1071
1072 @Override
1073 public void setChecked(boolean checked) {
1074 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001075
Alan Viverette467d6292014-08-12 15:13:19 -07001076 // Calling the super method may result in setChecked() getting called
1077 // recursively with a different value, so load the REAL value...
1078 checked = isChecked();
1079
Alan Viverette86453ff2013-09-26 14:46:08 -07001080 if (isAttachedToWindow() && isLaidOut()) {
1081 animateThumbToCheckedState(checked);
1082 } else {
1083 // Immediately move the thumb to the new position.
1084 cancelPositionAnimator();
1085 setThumbPosition(checked ? 1 : 0);
1086 }
Adam Powell12190b32010-11-28 19:07:53 -08001087 }
1088
1089 @Override
1090 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1091 super.onLayout(changed, left, top, right, bottom);
1092
Alan Viverette0c0dde72014-07-30 13:29:39 -07001093 int opticalInsetLeft = 0;
1094 int opticalInsetRight = 0;
1095 if (mThumbDrawable != null) {
1096 final Rect trackPadding = mTempRect;
1097 if (mTrackDrawable != null) {
1098 mTrackDrawable.getPadding(trackPadding);
1099 } else {
1100 trackPadding.setEmpty();
1101 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001102
Alan Viverette0c0dde72014-07-30 13:29:39 -07001103 final Insets insets = mThumbDrawable.getOpticalInsets();
1104 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1105 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001106 }
1107
Alan Viverette0c0dde72014-07-30 13:29:39 -07001108 final int switchRight;
1109 final int switchLeft;
1110 if (isLayoutRtl()) {
1111 switchLeft = getPaddingLeft() + opticalInsetLeft;
1112 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1113 } else {
1114 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1115 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1116 }
1117
1118 final int switchTop;
1119 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001120 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1121 default:
1122 case Gravity.TOP:
1123 switchTop = getPaddingTop();
1124 switchBottom = switchTop + mSwitchHeight;
1125 break;
1126
1127 case Gravity.CENTER_VERTICAL:
1128 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1129 mSwitchHeight / 2;
1130 switchBottom = switchTop + mSwitchHeight;
1131 break;
1132
1133 case Gravity.BOTTOM:
1134 switchBottom = getHeight() - getPaddingBottom();
1135 switchTop = switchBottom - mSwitchHeight;
1136 break;
1137 }
1138
1139 mSwitchLeft = switchLeft;
1140 mSwitchTop = switchTop;
1141 mSwitchBottom = switchBottom;
1142 mSwitchRight = switchRight;
1143 }
1144
1145 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001146 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001147 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001148 final int switchLeft = mSwitchLeft;
1149 final int switchTop = mSwitchTop;
1150 final int switchRight = mSwitchRight;
1151 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001152
1153 int thumbInitialLeft = switchLeft + getThumbOffset();
1154
1155 final Insets thumbInsets;
1156 if (mThumbDrawable != null) {
1157 thumbInsets = mThumbDrawable.getOpticalInsets();
1158 } else {
1159 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001160 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001161
Alan Viverette0c0dde72014-07-30 13:29:39 -07001162 // Layout the track.
1163 if (mTrackDrawable != null) {
1164 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001165
Alan Viverette0c0dde72014-07-30 13:29:39 -07001166 // Adjust thumb position for track padding.
1167 thumbInitialLeft += padding.left;
1168
1169 // If necessary, offset by the optical insets of the thumb asset.
1170 int trackLeft = switchLeft;
1171 int trackTop = switchTop;
1172 int trackRight = switchRight;
1173 int trackBottom = switchBottom;
1174 if (thumbInsets != Insets.NONE) {
1175 if (thumbInsets.left > padding.left) {
1176 trackLeft += thumbInsets.left - padding.left;
1177 }
1178 if (thumbInsets.top > padding.top) {
1179 trackTop += thumbInsets.top - padding.top;
1180 }
1181 if (thumbInsets.right > padding.right) {
1182 trackRight -= thumbInsets.right - padding.right;
1183 }
1184 if (thumbInsets.bottom > padding.bottom) {
1185 trackBottom -= thumbInsets.bottom - padding.bottom;
1186 }
1187 }
1188 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1189 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001190
Alan Viverette661e6362014-05-12 10:55:37 -07001191 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001192 if (mThumbDrawable != null) {
1193 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001194
1195 final int thumbLeft = thumbInitialLeft - padding.left;
1196 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001197 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001198
Alan Viverette4d065a02014-07-11 15:28:38 -07001199 final Drawable background = getBackground();
1200 if (background != null) {
1201 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1202 }
Alan Viverette61956602014-04-22 19:07:06 -07001203 }
1204
Alan Viverettead2f8e32014-05-16 13:28:33 -07001205 // Draw the background.
1206 super.draw(c);
1207 }
1208
1209 @Override
1210 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001211 super.onDraw(canvas);
1212
Alan Viverette4d065a02014-07-11 15:28:38 -07001213 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001214 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001215 if (trackDrawable != null) {
1216 trackDrawable.getPadding(padding);
1217 } else {
1218 padding.setEmpty();
1219 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001220
1221 final int switchTop = mSwitchTop;
1222 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001223 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001224 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001225
Alan Viverette4d065a02014-07-11 15:28:38 -07001226 final Drawable thumbDrawable = mThumbDrawable;
1227 if (trackDrawable != null) {
1228 if (mSplitTrack && thumbDrawable != null) {
1229 final Insets insets = thumbDrawable.getOpticalInsets();
1230 thumbDrawable.copyBounds(padding);
1231 padding.left += insets.left;
1232 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001233
Alan Viverette4d065a02014-07-11 15:28:38 -07001234 final int saveCount = canvas.save();
1235 canvas.clipRect(padding, Op.DIFFERENCE);
1236 trackDrawable.draw(canvas);
1237 canvas.restoreToCount(saveCount);
1238 } else {
1239 trackDrawable.draw(canvas);
1240 }
Alan Viverette661e6362014-05-12 10:55:37 -07001241 }
Alan Viverette61956602014-04-22 19:07:06 -07001242
1243 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001244
1245 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001246 thumbDrawable.draw(canvas);
1247 }
Adam Powell12190b32010-11-28 19:07:53 -08001248
Alan Viverette5876ff42014-03-03 17:40:46 -08001249 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001250 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001251 final int drawableState[] = getDrawableState();
1252 if (mTextColors != null) {
1253 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1254 }
1255 mTextPaint.drawableState = drawableState;
1256
Alan Viverette4d065a02014-07-11 15:28:38 -07001257 final int cX;
1258 if (thumbDrawable != null) {
1259 final Rect bounds = thumbDrawable.getBounds();
1260 cX = bounds.left + bounds.right;
1261 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001262 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001263 }
1264
1265 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001266 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1267 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001268 switchText.draw(canvas);
1269 }
Adam Powell12190b32010-11-28 19:07:53 -08001270
Alan Viverette5876ff42014-03-03 17:40:46 -08001271 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001272 }
1273
1274 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001275 public int getCompoundPaddingLeft() {
1276 if (!isLayoutRtl()) {
1277 return super.getCompoundPaddingLeft();
1278 }
1279 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1280 if (!TextUtils.isEmpty(getText())) {
1281 padding += mSwitchPadding;
1282 }
1283 return padding;
1284 }
1285
1286 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001287 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001288 if (isLayoutRtl()) {
1289 return super.getCompoundPaddingRight();
1290 }
Adam Powell12190b32010-11-28 19:07:53 -08001291 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1292 if (!TextUtils.isEmpty(getText())) {
1293 padding += mSwitchPadding;
1294 }
1295 return padding;
1296 }
1297
Alan Viverettecc2688d2013-09-17 17:00:12 -07001298 /**
1299 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001300 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001301 *
1302 * @return thumb offset
1303 */
1304 private int getThumbOffset() {
1305 final float thumbPosition;
1306 if (isLayoutRtl()) {
1307 thumbPosition = 1 - mThumbPosition;
1308 } else {
1309 thumbPosition = mThumbPosition;
1310 }
1311 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1312 }
1313
Adam Powell12190b32010-11-28 19:07:53 -08001314 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001315 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001316 final Rect padding = mTempRect;
1317 mTrackDrawable.getPadding(padding);
1318
1319 final Insets insets;
1320 if (mThumbDrawable != null) {
1321 insets = mThumbDrawable.getOpticalInsets();
1322 } else {
1323 insets = Insets.NONE;
1324 }
1325
1326 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1327 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001328 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001329 return 0;
1330 }
Adam Powell12190b32010-11-28 19:07:53 -08001331 }
1332
1333 @Override
1334 protected int[] onCreateDrawableState(int extraSpace) {
1335 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1336 if (isChecked()) {
1337 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1338 }
1339 return drawableState;
1340 }
1341
1342 @Override
1343 protected void drawableStateChanged() {
1344 super.drawableStateChanged();
1345
Alan Viverettead0020f2015-09-04 10:10:42 -04001346 final int[] state = getDrawableState();
1347 boolean changed = false;
Adam Powell12190b32010-11-28 19:07:53 -08001348
Alan Viverettead0020f2015-09-04 10:10:42 -04001349 final Drawable thumbDrawable = mThumbDrawable;
1350 if (thumbDrawable != null && thumbDrawable.isStateful()) {
1351 changed |= thumbDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001352 }
1353
Alan Viverettead0020f2015-09-04 10:10:42 -04001354 final Drawable trackDrawable = mTrackDrawable;
1355 if (trackDrawable != null && trackDrawable.isStateful()) {
1356 changed |= trackDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001357 }
Adam Powell12190b32010-11-28 19:07:53 -08001358
Alan Viverettead0020f2015-09-04 10:10:42 -04001359 if (changed) {
1360 invalidate();
1361 }
Adam Powell12190b32010-11-28 19:07:53 -08001362 }
1363
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001364 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001365 public void drawableHotspotChanged(float x, float y) {
1366 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001367
1368 if (mThumbDrawable != null) {
1369 mThumbDrawable.setHotspot(x, y);
1370 }
1371
1372 if (mTrackDrawable != null) {
1373 mTrackDrawable.setHotspot(x, y);
1374 }
1375 }
1376
Adam Powell12190b32010-11-28 19:07:53 -08001377 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -05001378 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell12190b32010-11-28 19:07:53 -08001379 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1380 }
1381
1382 @Override
1383 public void jumpDrawablesToCurrentState() {
1384 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001385
1386 if (mThumbDrawable != null) {
1387 mThumbDrawable.jumpToCurrentState();
1388 }
1389
1390 if (mTrackDrawable != null) {
1391 mTrackDrawable.jumpToCurrentState();
1392 }
1393
Alan Viveretteee20d772015-09-23 14:23:21 -04001394 if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001395 mPositionAnimator.end();
1396 mPositionAnimator = null;
1397 }
Adam Powell12190b32010-11-28 19:07:53 -08001398 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001399
1400 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001401 public CharSequence getAccessibilityClassName() {
1402 return Switch.class.getName();
1403 }
1404
1405 @Override
Dianne Hackborn49b043f2015-05-07 14:21:38 -07001406 public void onProvideStructure(ViewStructure structure) {
1407 super.onProvideStructure(structure);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001408 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1409 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001410 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001411 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001412 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001413 } else {
1414 StringBuilder newText = new StringBuilder();
1415 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001416 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001417 }
James Cook5cfaae42015-05-28 15:52:44 -07001418 // The style of the label text is provided via the base TextView class. This is more
1419 // relevant than the style of the (optional) on/off text on the switch button itself,
1420 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001421 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001422 }
1423
Alan Viverettea54956a2015-01-07 16:05:02 -08001424 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001425 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001426 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1427 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001428 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1429 if (!TextUtils.isEmpty(switchText)) {
1430 CharSequence oldText = info.getText();
1431 if (TextUtils.isEmpty(oldText)) {
1432 info.setText(switchText);
1433 } else {
1434 StringBuilder newText = new StringBuilder();
1435 newText.append(oldText).append(' ').append(switchText);
1436 info.setText(newText);
1437 }
1438 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001439 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001440
1441 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1442 @Override
1443 public Float get(Switch object) {
1444 return object.mThumbPosition;
1445 }
1446
1447 @Override
1448 public void setValue(Switch object, float value) {
1449 object.setThumbPosition(value);
1450 }
1451 };
Adam Powell12190b32010-11-28 19:07:53 -08001452}