blob: 2e1e96365f2a79c6c137c15a203bf6961b3d456d [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;
Alan Viverette661e6362014-05-12 10:55:37 -070033import android.graphics.Region.Op;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070034import android.graphics.Typeface;
Adam Powell12190b32010-11-28 19:07:53 -080035import android.graphics.drawable.Drawable;
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -070036import android.os.Build.VERSION_CODES;
Adam Powell12190b32010-11-28 19:07:53 -080037import android.text.Layout;
38import android.text.StaticLayout;
39import android.text.TextPaint;
40import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040041import android.text.method.AllCapsTransformationMethod;
42import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080043import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070044import android.util.FloatProperty;
45import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080046import android.view.Gravity;
47import android.view.MotionEvent;
Alan Viveretted4e77902014-10-27 17:50:51 -070048import android.view.SoundEffectConstants;
Adam Powell12190b32010-11-28 19:07:53 -080049import android.view.VelocityTracker;
50import android.view.ViewConfiguration;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070051import android.view.ViewStructure;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070052import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080053import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080054
Adam Powellbe0a4532010-11-29 17:47:48 -080055import com.android.internal.R;
56
Adam Powell12190b32010-11-28 19:07:53 -080057/**
58 * A Switch is a two-state toggle switch widget that can select between two
59 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070060 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
61 * property controls the text displayed in the label for the switch, whereas the
62 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
63 * controls the text on the thumb. Similarly, the
64 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
65 * setTypeface() methods control the typeface and style of label text, whereas the
66 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
Andrew Solovay07e70212015-07-08 12:49:21 -070067 * the related setSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080068 *
Mark Lu34040322016-08-30 18:12:30 -070069 * <p>{@link android.support.v7.widget.SwitchCompat} is a version of
70 * the Switch widget which runs on devices back to API 7.</p>
71 *
Scott Main4c359b72012-07-24 15:51:27 -070072 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
73 * guide.</p>
74 *
75 * @attr ref android.R.styleable#Switch_textOn
76 * @attr ref android.R.styleable#Switch_textOff
77 * @attr ref android.R.styleable#Switch_switchMinWidth
78 * @attr ref android.R.styleable#Switch_switchPadding
79 * @attr ref android.R.styleable#Switch_switchTextAppearance
80 * @attr ref android.R.styleable#Switch_thumb
81 * @attr ref android.R.styleable#Switch_thumbTextPadding
82 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080083 */
84public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070085 private static final int THUMB_ANIMATION_DURATION = 250;
86
Adam Powell12190b32010-11-28 19:07:53 -080087 private static final int TOUCH_MODE_IDLE = 0;
88 private static final int TOUCH_MODE_DOWN = 1;
89 private static final int TOUCH_MODE_DRAGGING = 2;
90
91 // Enum for the "typeface" XML parameter.
92 private static final int SANS = 1;
93 private static final int SERIF = 2;
94 private static final int MONOSPACE = 3;
95
96 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080097 private ColorStateList mThumbTintList = null;
98 private PorterDuff.Mode mThumbTintMode = null;
99 private boolean mHasThumbTint = false;
100 private boolean mHasThumbTintMode = false;
101
Adam Powell12190b32010-11-28 19:07:53 -0800102 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -0800103 private ColorStateList mTrackTintList = null;
104 private PorterDuff.Mode mTrackTintMode = null;
105 private boolean mHasTrackTint = false;
106 private boolean mHasTrackTintMode = false;
107
Adam Powell12190b32010-11-28 19:07:53 -0800108 private int mThumbTextPadding;
109 private int mSwitchMinWidth;
110 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700111 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800112 private CharSequence mTextOn;
113 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700114 private boolean mShowText;
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700115 private boolean mUseFallbackLineSpacing;
Adam Powell12190b32010-11-28 19:07:53 -0800116
117 private int mTouchMode;
118 private int mTouchSlop;
119 private float mTouchX;
120 private float mTouchY;
121 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
122 private int mMinFlingVelocity;
123
124 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700125
Alan Viverette0c0dde72014-07-30 13:29:39 -0700126 /**
127 * Width required to draw the switch track and thumb. Includes padding and
128 * optical bounds for both the track and thumb.
129 */
130 private int mSwitchWidth;
131
132 /**
133 * Height required to draw the switch track and thumb. Includes padding and
134 * optical bounds for both the track and thumb.
135 */
136 private int mSwitchHeight;
137
138 /**
139 * Width of the thumb's content region. Does not include padding or
140 * optical bounds.
141 */
142 private int mThumbWidth;
143
144 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800145 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700146
147 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800148 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700149
150 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800151 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700152
153 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800154 private int mSwitchBottom;
155
156 private TextPaint mTextPaint;
157 private ColorStateList mTextColors;
158 private Layout mOnLayout;
159 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400160 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700161 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800162
Adam Powellbe0a4532010-11-29 17:47:48 -0800163 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800164 private final Rect mTempRect = new Rect();
165
166 private static final int[] CHECKED_STATE_SET = {
167 R.attr.state_checked
168 };
169
170 /**
171 * Construct a new Switch with default styling.
172 *
173 * @param context The Context that will determine this widget's theming.
174 */
175 public Switch(Context context) {
176 this(context, null);
177 }
178
179 /**
180 * Construct a new Switch with default styling, overriding specific style
181 * attributes as requested.
182 *
183 * @param context The Context that will determine this widget's theming.
184 * @param attrs Specification of attributes that should deviate from default styling.
185 */
186 public Switch(Context context, AttributeSet attrs) {
187 this(context, attrs, com.android.internal.R.attr.switchStyle);
188 }
189
190 /**
191 * Construct a new Switch with a default style determined by the given theme attribute,
192 * overriding specific style attributes as requested.
193 *
194 * @param context The Context that will determine this widget's theming.
195 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700196 * @param defStyleAttr An attribute in the current theme that contains a
197 * reference to a style resource that supplies default values for
198 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800199 */
Alan Viverette617feb92013-09-09 18:09:13 -0700200 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
201 this(context, attrs, defStyleAttr, 0);
202 }
203
204
205 /**
206 * Construct a new Switch with a default style determined by the given theme
207 * attribute or style resource, overriding specific style attributes as
208 * requested.
209 *
210 * @param context The Context that will determine this widget's theming.
211 * @param attrs Specification of attributes that should deviate from the
212 * default styling.
213 * @param defStyleAttr An attribute in the current theme that contains a
214 * reference to a style resource that supplies default values for
215 * the view. Can be 0 to not look for defaults.
216 * @param defStyleRes A resource identifier of a style resource that
217 * supplies default values for the view, used only if
218 * defStyleAttr is 0 or can not be found in the theme. Can be 0
219 * to not look for defaults.
220 */
221 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
222 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800223
Chris Craik6a49dde2015-05-12 10:28:14 -0700224 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700225
226 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800227 mTextPaint.density = res.getDisplayMetrics().density;
228 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
229
Alan Viverette617feb92013-09-09 18:09:13 -0700230 final TypedArray a = context.obtainStyledAttributes(
231 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700232 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700233 if (mThumbDrawable != null) {
234 mThumbDrawable.setCallback(this);
235 }
Chet Haase150176d2011-08-26 09:54:06 -0700236 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700237 if (mTrackDrawable != null) {
238 mTrackDrawable.setCallback(this);
239 }
Adam Powell12190b32010-11-28 19:07:53 -0800240 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
241 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700242 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800243 mThumbTextPadding = a.getDimensionPixelSize(
244 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
245 mSwitchMinWidth = a.getDimensionPixelSize(
246 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
247 mSwitchPadding = a.getDimensionPixelSize(
248 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700249 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800250
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700251 // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES.
252 // STOPSHIP if the above TODO is not done.
253 mUseFallbackLineSpacing =
254 context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT;
255
Jun Mukai9fb302c2015-06-24 18:31:06 -0700256 ColorStateList thumbTintList = a.getColorStateList(
257 com.android.internal.R.styleable.Switch_thumbTint);
258 if (thumbTintList != null) {
259 mThumbTintList = thumbTintList;
260 mHasThumbTint = true;
261 }
262 PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
263 a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
264 if (mThumbTintMode != thumbTintMode) {
265 mThumbTintMode = thumbTintMode;
266 mHasThumbTintMode = true;
267 }
268 if (mHasThumbTint || mHasThumbTintMode) {
269 applyThumbTint();
270 }
271
272 ColorStateList trackTintList = a.getColorStateList(
273 com.android.internal.R.styleable.Switch_trackTint);
274 if (trackTintList != null) {
275 mTrackTintList = trackTintList;
276 mHasTrackTint = true;
277 }
278 PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
279 a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
280 if (mTrackTintMode != trackTintMode) {
281 mTrackTintMode = trackTintMode;
282 mHasTrackTintMode = true;
283 }
284 if (mHasTrackTint || mHasTrackTintMode) {
285 applyTrackTint();
286 }
287
Alan Viverette661e6362014-05-12 10:55:37 -0700288 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800289 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
290 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700291 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800292 }
293 a.recycle();
294
Alan Viverette661e6362014-05-12 10:55:37 -0700295 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800296 mTouchSlop = config.getScaledTouchSlop();
297 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
298
299 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700300 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800301 setChecked(isChecked());
302 }
303
304 /**
305 * Sets the switch text color, size, style, hint color, and highlight color
306 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800307 *
308 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800309 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700310 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800311 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700312 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800313 com.android.internal.R.styleable.TextAppearance);
314
315 ColorStateList colors;
316 int ts;
317
318 colors = appearance.getColorStateList(com.android.internal.R.styleable.
319 TextAppearance_textColor);
320 if (colors != null) {
321 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700322 } else {
323 // If no color set in TextAppearance, default to the view's textColor
324 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800325 }
326
327 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
328 TextAppearance_textSize, 0);
329 if (ts != 0) {
330 if (ts != mTextPaint.getTextSize()) {
331 mTextPaint.setTextSize(ts);
332 requestLayout();
333 }
334 }
335
336 int typefaceIndex, styleIndex;
337
338 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
339 TextAppearance_typeface, -1);
340 styleIndex = appearance.getInt(com.android.internal.R.styleable.
341 TextAppearance_textStyle, -1);
342
343 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
344
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400345 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
346 TextAppearance_textAllCaps, false);
347 if (allCaps) {
348 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
349 mSwitchTransformationMethod.setLengthChangesAllowed(true);
350 } else {
351 mSwitchTransformationMethod = null;
352 }
353
Adam Powell12190b32010-11-28 19:07:53 -0800354 appearance.recycle();
355 }
356
357 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
358 Typeface tf = null;
359 switch (typefaceIndex) {
360 case SANS:
361 tf = Typeface.SANS_SERIF;
362 break;
363
364 case SERIF:
365 tf = Typeface.SERIF;
366 break;
367
368 case MONOSPACE:
369 tf = Typeface.MONOSPACE;
370 break;
371 }
372
373 setSwitchTypeface(tf, styleIndex);
374 }
375
376 /**
377 * Sets the typeface and style in which the text should be displayed on the
378 * switch, and turns on the fake bold and italic bits in the Paint if the
379 * Typeface that you provided does not have all the bits in the
380 * style that you specified.
381 */
382 public void setSwitchTypeface(Typeface tf, int style) {
383 if (style > 0) {
384 if (tf == null) {
385 tf = Typeface.defaultFromStyle(style);
386 } else {
387 tf = Typeface.create(tf, style);
388 }
389
390 setSwitchTypeface(tf);
391 // now compute what (if any) algorithmic styling is needed
392 int typefaceStyle = tf != null ? tf.getStyle() : 0;
393 int need = style & ~typefaceStyle;
394 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
395 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
396 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700397 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800398 mTextPaint.setTextSkewX(0);
399 setSwitchTypeface(tf);
400 }
401 }
402
403 /**
Chet Haase150176d2011-08-26 09:54:06 -0700404 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800405 * Note that not all Typeface families actually have bold and italic
406 * variants, so you may need to use
407 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
408 * that you actually want.
409 *
410 * @attr ref android.R.styleable#TextView_typeface
411 * @attr ref android.R.styleable#TextView_textStyle
412 */
413 public void setSwitchTypeface(Typeface tf) {
414 if (mTextPaint.getTypeface() != tf) {
415 mTextPaint.setTypeface(tf);
416
417 requestLayout();
418 invalidate();
419 }
420 }
421
422 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800423 * Set the amount of horizontal padding between the switch and the associated text.
424 *
425 * @param pixels Amount of padding in pixels
426 *
427 * @attr ref android.R.styleable#Switch_switchPadding
428 */
429 public void setSwitchPadding(int pixels) {
430 mSwitchPadding = pixels;
431 requestLayout();
432 }
433
434 /**
435 * Get the amount of horizontal padding between the switch and the associated text.
436 *
437 * @return Amount of padding in pixels
438 *
439 * @attr ref android.R.styleable#Switch_switchPadding
440 */
441 public int getSwitchPadding() {
442 return mSwitchPadding;
443 }
444
445 /**
446 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
447 * of this value and its measured width as determined by the switch drawables and text used.
448 *
449 * @param pixels Minimum width of the switch in pixels
450 *
451 * @attr ref android.R.styleable#Switch_switchMinWidth
452 */
453 public void setSwitchMinWidth(int pixels) {
454 mSwitchMinWidth = pixels;
455 requestLayout();
456 }
457
458 /**
459 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
460 * of this value and its measured width as determined by the switch drawables and text used.
461 *
462 * @return Minimum width of the switch in pixels
463 *
464 * @attr ref android.R.styleable#Switch_switchMinWidth
465 */
466 public int getSwitchMinWidth() {
467 return mSwitchMinWidth;
468 }
469
470 /**
471 * Set the horizontal padding around the text drawn on the switch itself.
472 *
473 * @param pixels Horizontal padding for switch thumb text in pixels
474 *
475 * @attr ref android.R.styleable#Switch_thumbTextPadding
476 */
477 public void setThumbTextPadding(int pixels) {
478 mThumbTextPadding = pixels;
479 requestLayout();
480 }
481
482 /**
483 * Get the horizontal padding around the text drawn on the switch itself.
484 *
485 * @return Horizontal padding for switch thumb text in pixels
486 *
487 * @attr ref android.R.styleable#Switch_thumbTextPadding
488 */
489 public int getThumbTextPadding() {
490 return mThumbTextPadding;
491 }
492
493 /**
494 * Set the drawable used for the track that the switch slides within.
495 *
496 * @param track Track drawable
497 *
498 * @attr ref android.R.styleable#Switch_track
499 */
500 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700501 if (mTrackDrawable != null) {
502 mTrackDrawable.setCallback(null);
503 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800504 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700505 if (track != null) {
506 track.setCallback(this);
507 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800508 requestLayout();
509 }
510
511 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800512 * Set the drawable used for the track that the switch slides within.
513 *
Adam Powelldca510e2012-03-08 20:06:39 -0800514 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800515 *
516 * @attr ref android.R.styleable#Switch_track
517 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700518 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800519 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800520 }
521
522 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800523 * Get the drawable used for the track that the switch slides within.
524 *
525 * @return Track drawable
526 *
527 * @attr ref android.R.styleable#Switch_track
528 */
529 public Drawable getTrackDrawable() {
530 return mTrackDrawable;
531 }
532
533 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800534 * Applies a tint to the track drawable. Does not modify the current
535 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
536 * <p>
537 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
538 * automatically mutate the drawable and apply the specified tint and tint
539 * mode using {@link Drawable#setTintList(ColorStateList)}.
540 *
541 * @param tint the tint to apply, may be {@code null} to clear tint
542 *
543 * @attr ref android.R.styleable#Switch_trackTint
544 * @see #getTrackTintList()
545 * @see Drawable#setTintList(ColorStateList)
546 */
547 public void setTrackTintList(@Nullable ColorStateList tint) {
548 mTrackTintList = tint;
549 mHasTrackTint = true;
550
551 applyTrackTint();
552 }
553
554 /**
555 * @return the tint applied to the track drawable
556 * @attr ref android.R.styleable#Switch_trackTint
557 * @see #setTrackTintList(ColorStateList)
558 */
559 @Nullable
560 public ColorStateList getTrackTintList() {
561 return mTrackTintList;
562 }
563
564 /**
565 * Specifies the blending mode used to apply the tint specified by
566 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
567 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
568 *
569 * @param tintMode the blending mode used to apply the tint, may be
570 * {@code null} to clear tint
571 * @attr ref android.R.styleable#Switch_trackTintMode
572 * @see #getTrackTintMode()
573 * @see Drawable#setTintMode(PorterDuff.Mode)
574 */
575 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
576 mTrackTintMode = tintMode;
577 mHasTrackTintMode = true;
578
579 applyTrackTint();
580 }
581
582 /**
583 * @return the blending mode used to apply the tint to the track
584 * drawable
585 * @attr ref android.R.styleable#Switch_trackTintMode
586 * @see #setTrackTintMode(PorterDuff.Mode)
587 */
588 @Nullable
589 public PorterDuff.Mode getTrackTintMode() {
590 return mTrackTintMode;
591 }
592
593 private void applyTrackTint() {
594 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
595 mTrackDrawable = mTrackDrawable.mutate();
596
597 if (mHasTrackTint) {
598 mTrackDrawable.setTintList(mTrackTintList);
599 }
600
601 if (mHasTrackTintMode) {
602 mTrackDrawable.setTintMode(mTrackTintMode);
603 }
604
605 // The drawable (or one of its children) may not have been
606 // stateful before applying the tint, so let's try again.
607 if (mTrackDrawable.isStateful()) {
608 mTrackDrawable.setState(getDrawableState());
609 }
610 }
611 }
612
613 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800614 * Set the drawable used for the switch "thumb" - the piece that the user
615 * can physically touch and drag along the track.
616 *
617 * @param thumb Thumb drawable
618 *
619 * @attr ref android.R.styleable#Switch_thumb
620 */
621 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700622 if (mThumbDrawable != null) {
623 mThumbDrawable.setCallback(null);
624 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800625 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700626 if (thumb != null) {
627 thumb.setCallback(this);
628 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800629 requestLayout();
630 }
631
632 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800633 * Set the drawable used for the switch "thumb" - the piece that the user
634 * can physically touch and drag along the track.
635 *
Adam Powelldca510e2012-03-08 20:06:39 -0800636 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800637 *
638 * @attr ref android.R.styleable#Switch_thumb
639 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700640 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800641 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800642 }
643
644 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800645 * Get the drawable used for the switch "thumb" - the piece that the user
646 * can physically touch and drag along the track.
647 *
648 * @return Thumb drawable
649 *
650 * @attr ref android.R.styleable#Switch_thumb
651 */
652 public Drawable getThumbDrawable() {
653 return mThumbDrawable;
654 }
655
656 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800657 * Applies a tint to the thumb drawable. Does not modify the current
658 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
659 * <p>
660 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
661 * automatically mutate the drawable and apply the specified tint and tint
662 * mode using {@link Drawable#setTintList(ColorStateList)}.
663 *
664 * @param tint the tint to apply, may be {@code null} to clear tint
665 *
666 * @attr ref android.R.styleable#Switch_thumbTint
667 * @see #getThumbTintList()
668 * @see Drawable#setTintList(ColorStateList)
669 */
670 public void setThumbTintList(@Nullable ColorStateList tint) {
671 mThumbTintList = tint;
672 mHasThumbTint = true;
673
674 applyThumbTint();
675 }
676
677 /**
678 * @return the tint applied to the thumb drawable
679 * @attr ref android.R.styleable#Switch_thumbTint
680 * @see #setThumbTintList(ColorStateList)
681 */
682 @Nullable
683 public ColorStateList getThumbTintList() {
684 return mThumbTintList;
685 }
686
687 /**
688 * Specifies the blending mode used to apply the tint specified by
689 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
690 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
691 *
692 * @param tintMode the blending mode used to apply the tint, may be
693 * {@code null} to clear tint
694 * @attr ref android.R.styleable#Switch_thumbTintMode
695 * @see #getThumbTintMode()
696 * @see Drawable#setTintMode(PorterDuff.Mode)
697 */
698 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
699 mThumbTintMode = tintMode;
700 mHasThumbTintMode = true;
701
702 applyThumbTint();
703 }
704
705 /**
706 * @return the blending mode used to apply the tint to the thumb
707 * drawable
708 * @attr ref android.R.styleable#Switch_thumbTintMode
709 * @see #setThumbTintMode(PorterDuff.Mode)
710 */
711 @Nullable
712 public PorterDuff.Mode getThumbTintMode() {
713 return mThumbTintMode;
714 }
715
716 private void applyThumbTint() {
717 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
718 mThumbDrawable = mThumbDrawable.mutate();
719
720 if (mHasThumbTint) {
721 mThumbDrawable.setTintList(mThumbTintList);
722 }
723
724 if (mHasThumbTintMode) {
725 mThumbDrawable.setTintMode(mThumbTintMode);
726 }
727
728 // The drawable (or one of its children) may not have been
729 // stateful before applying the tint, so let's try again.
730 if (mThumbDrawable.isStateful()) {
731 mThumbDrawable.setState(getDrawableState());
732 }
733 }
734 }
735
736 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700737 * Specifies whether the track should be split by the thumb. When true,
738 * the thumb's optical bounds will be clipped out of the track drawable,
739 * then the thumb will be drawn into the resulting gap.
740 *
741 * @param splitTrack Whether the track should be split by the thumb
742 *
743 * @attr ref android.R.styleable#Switch_splitTrack
744 */
745 public void setSplitTrack(boolean splitTrack) {
746 mSplitTrack = splitTrack;
747 invalidate();
748 }
749
750 /**
751 * Returns whether the track should be split by the thumb.
752 *
753 * @attr ref android.R.styleable#Switch_splitTrack
754 */
755 public boolean getSplitTrack() {
756 return mSplitTrack;
757 }
758
759 /**
Chet Haase150176d2011-08-26 09:54:06 -0700760 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800761 *
762 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800763 */
764 public CharSequence getTextOn() {
765 return mTextOn;
766 }
767
768 /**
Chet Haase150176d2011-08-26 09:54:06 -0700769 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800770 *
771 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800772 */
773 public void setTextOn(CharSequence textOn) {
774 mTextOn = textOn;
775 requestLayout();
776 }
777
778 /**
Chet Haase150176d2011-08-26 09:54:06 -0700779 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800780 *
781 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800782 */
783 public CharSequence getTextOff() {
784 return mTextOff;
785 }
786
787 /**
Chet Haase150176d2011-08-26 09:54:06 -0700788 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800789 *
790 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800791 */
792 public void setTextOff(CharSequence textOff) {
793 mTextOff = textOff;
794 requestLayout();
795 }
796
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700797 /**
798 * Sets whether the on/off text should be displayed.
799 *
800 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700801 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700802 */
803 public void setShowText(boolean showText) {
804 if (mShowText != showText) {
805 mShowText = showText;
806 requestLayout();
807 }
808 }
809
810 /**
811 * @return whether the on/off text should be displayed
Alan Viverette0c0dde72014-07-30 13:29:39 -0700812 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700813 */
814 public boolean getShowText() {
815 return mShowText;
816 }
817
Adam Powell12190b32010-11-28 19:07:53 -0800818 @Override
819 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700820 if (mShowText) {
821 if (mOnLayout == null) {
822 mOnLayout = makeLayout(mTextOn);
823 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800824
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700825 if (mOffLayout == null) {
826 mOffLayout = makeLayout(mTextOff);
827 }
Adam Powell12190b32010-11-28 19:07:53 -0800828 }
829
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000830 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700831 final int thumbWidth;
832 final int thumbHeight;
833 if (mThumbDrawable != null) {
834 // Cached thumb width does not include padding.
835 mThumbDrawable.getPadding(padding);
836 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
837 thumbHeight = mThumbDrawable.getIntrinsicHeight();
838 } else {
839 thumbWidth = 0;
840 thumbHeight = 0;
841 }
842
843 final int maxTextWidth;
844 if (mShowText) {
845 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
846 + mThumbTextPadding * 2;
847 } else {
848 maxTextWidth = 0;
849 }
850
851 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
852
853 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700854 if (mTrackDrawable != null) {
855 mTrackDrawable.getPadding(padding);
856 trackHeight = mTrackDrawable.getIntrinsicHeight();
857 } else {
858 padding.setEmpty();
859 trackHeight = 0;
860 }
861
Alan Viverette0c0dde72014-07-30 13:29:39 -0700862 // Adjust left and right padding to ensure there's enough room for the
863 // thumb's padding (when present).
864 int paddingLeft = padding.left;
865 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700866 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700867 final Insets inset = mThumbDrawable.getOpticalInsets();
868 paddingLeft = Math.max(paddingLeft, inset.left);
869 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700870 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800871
Adam Powell12190b32010-11-28 19:07:53 -0800872 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700873 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700874 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800875 mSwitchWidth = switchWidth;
876 mSwitchHeight = switchHeight;
877
878 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700879
Adam Powell12190b32010-11-28 19:07:53 -0800880 final int measuredHeight = getMeasuredHeight();
881 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800882 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800883 }
884 }
885
Alan Viverettea54956a2015-01-07 16:05:02 -0800886 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700887 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800888 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
889 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700890
891 final CharSequence text = isChecked() ? mTextOn : mTextOff;
892 if (text != null) {
893 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700894 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700895 }
896
Adam Powell12190b32010-11-28 19:07:53 -0800897 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400898 final CharSequence transformed = (mSwitchTransformationMethod != null)
899 ? mSwitchTransformationMethod.getTransformation(text, this)
900 : text;
901
Siyamed Sinir79bf9d12016-05-18 19:57:52 -0700902 int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
903 transformed.length(), mTextPaint, getTextDirectionHeuristic()));
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700904 return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width)
905 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
906 .build();
Adam Powell12190b32010-11-28 19:07:53 -0800907 }
908
909 /**
910 * @return true if (x, y) is within the target area of the switch thumb
911 */
912 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800913 if (mThumbDrawable == null) {
914 return false;
915 }
916
Alan Viverettecc2688d2013-09-17 17:00:12 -0700917 // Relies on mTempRect, MUST be called first!
918 final int thumbOffset = getThumbOffset();
919
Adam Powell12190b32010-11-28 19:07:53 -0800920 mThumbDrawable.getPadding(mTempRect);
921 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700922 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800923 final int thumbRight = thumbLeft + mThumbWidth +
924 mTempRect.left + mTempRect.right + mTouchSlop;
925 final int thumbBottom = mSwitchBottom + mTouchSlop;
926 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
927 }
928
929 @Override
930 public boolean onTouchEvent(MotionEvent ev) {
931 mVelocityTracker.addMovement(ev);
932 final int action = ev.getActionMasked();
933 switch (action) {
934 case MotionEvent.ACTION_DOWN: {
935 final float x = ev.getX();
936 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700937 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800938 mTouchMode = TOUCH_MODE_DOWN;
939 mTouchX = x;
940 mTouchY = y;
941 }
942 break;
943 }
944
945 case MotionEvent.ACTION_MOVE: {
946 switch (mTouchMode) {
947 case TOUCH_MODE_IDLE:
948 // Didn't target the thumb, treat normally.
949 break;
950
951 case TOUCH_MODE_DOWN: {
952 final float x = ev.getX();
953 final float y = ev.getY();
954 if (Math.abs(x - mTouchX) > mTouchSlop ||
955 Math.abs(y - mTouchY) > mTouchSlop) {
956 mTouchMode = TOUCH_MODE_DRAGGING;
957 getParent().requestDisallowInterceptTouchEvent(true);
958 mTouchX = x;
959 mTouchY = y;
960 return true;
961 }
962 break;
963 }
964
965 case TOUCH_MODE_DRAGGING: {
966 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700967 final int thumbScrollRange = getThumbScrollRange();
968 final float thumbScrollOffset = x - mTouchX;
969 float dPos;
970 if (thumbScrollRange != 0) {
971 dPos = thumbScrollOffset / thumbScrollRange;
972 } else {
973 // If the thumb scroll range is empty, just use the
974 // movement direction to snap on or off.
975 dPos = thumbScrollOffset > 0 ? 1 : -1;
976 }
977 if (isLayoutRtl()) {
978 dPos = -dPos;
979 }
980 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800981 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800982 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700983 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800984 }
985 return true;
986 }
987 }
988 break;
989 }
990
991 case MotionEvent.ACTION_UP:
992 case MotionEvent.ACTION_CANCEL: {
993 if (mTouchMode == TOUCH_MODE_DRAGGING) {
994 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700995 // Allow super class to handle pressed state, etc.
996 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800997 return true;
998 }
999 mTouchMode = TOUCH_MODE_IDLE;
1000 mVelocityTracker.clear();
1001 break;
1002 }
1003 }
1004
1005 return super.onTouchEvent(ev);
1006 }
1007
1008 private void cancelSuperTouch(MotionEvent ev) {
1009 MotionEvent cancel = MotionEvent.obtain(ev);
1010 cancel.setAction(MotionEvent.ACTION_CANCEL);
1011 super.onTouchEvent(cancel);
1012 cancel.recycle();
1013 }
1014
1015 /**
1016 * Called from onTouchEvent to end a drag operation.
1017 *
1018 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1019 */
1020 private void stopDrag(MotionEvent ev) {
1021 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001022
Alan Viverette86453ff2013-09-26 14:46:08 -07001023 // Commit the change if the event is up and not canceled and the switch
1024 // has not been disabled during the drag.
1025 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001026 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001027 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001028 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001029 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001030 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001031 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001032 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001033 } else {
1034 newState = getTargetCheckedState();
1035 }
Adam Powell12190b32010-11-28 19:07:53 -08001036 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001037 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001038 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001039
Alan Viveretted4e77902014-10-27 17:50:51 -07001040 if (newState != oldState) {
1041 playSoundEffect(SoundEffectConstants.CLICK);
Alan Viveretted4e77902014-10-27 17:50:51 -07001042 }
Alan Viverette03306a02015-08-07 11:34:25 -04001043 // Always call setChecked so that the thumb is moved back to the correct edge
1044 setChecked(newState);
Alan Viverette86453ff2013-09-26 14:46:08 -07001045 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001046 }
1047
1048 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001049 final float targetPosition = newCheckedState ? 1 : 0;
1050 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1051 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1052 mPositionAnimator.setAutoCancel(true);
1053 mPositionAnimator.start();
1054 }
1055
1056 private void cancelPositionAnimator() {
1057 if (mPositionAnimator != null) {
1058 mPositionAnimator.cancel();
1059 }
Adam Powell12190b32010-11-28 19:07:53 -08001060 }
1061
1062 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001063 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001064 }
1065
Alan Viverettecc2688d2013-09-17 17:00:12 -07001066 /**
1067 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1068 *
1069 * @param position new position between [0,1]
1070 */
1071 private void setThumbPosition(float position) {
1072 mThumbPosition = position;
1073 invalidate();
1074 }
1075
1076 @Override
1077 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001078 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001079 }
1080
1081 @Override
1082 public void setChecked(boolean checked) {
1083 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001084
Alan Viverette467d6292014-08-12 15:13:19 -07001085 // Calling the super method may result in setChecked() getting called
1086 // recursively with a different value, so load the REAL value...
1087 checked = isChecked();
1088
Alan Viverette86453ff2013-09-26 14:46:08 -07001089 if (isAttachedToWindow() && isLaidOut()) {
1090 animateThumbToCheckedState(checked);
1091 } else {
1092 // Immediately move the thumb to the new position.
1093 cancelPositionAnimator();
1094 setThumbPosition(checked ? 1 : 0);
1095 }
Adam Powell12190b32010-11-28 19:07:53 -08001096 }
1097
1098 @Override
1099 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1100 super.onLayout(changed, left, top, right, bottom);
1101
Alan Viverette0c0dde72014-07-30 13:29:39 -07001102 int opticalInsetLeft = 0;
1103 int opticalInsetRight = 0;
1104 if (mThumbDrawable != null) {
1105 final Rect trackPadding = mTempRect;
1106 if (mTrackDrawable != null) {
1107 mTrackDrawable.getPadding(trackPadding);
1108 } else {
1109 trackPadding.setEmpty();
1110 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001111
Alan Viverette0c0dde72014-07-30 13:29:39 -07001112 final Insets insets = mThumbDrawable.getOpticalInsets();
1113 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1114 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001115 }
1116
Alan Viverette0c0dde72014-07-30 13:29:39 -07001117 final int switchRight;
1118 final int switchLeft;
1119 if (isLayoutRtl()) {
1120 switchLeft = getPaddingLeft() + opticalInsetLeft;
1121 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1122 } else {
1123 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1124 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1125 }
1126
1127 final int switchTop;
1128 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001129 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1130 default:
1131 case Gravity.TOP:
1132 switchTop = getPaddingTop();
1133 switchBottom = switchTop + mSwitchHeight;
1134 break;
1135
1136 case Gravity.CENTER_VERTICAL:
1137 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1138 mSwitchHeight / 2;
1139 switchBottom = switchTop + mSwitchHeight;
1140 break;
1141
1142 case Gravity.BOTTOM:
1143 switchBottom = getHeight() - getPaddingBottom();
1144 switchTop = switchBottom - mSwitchHeight;
1145 break;
1146 }
1147
1148 mSwitchLeft = switchLeft;
1149 mSwitchTop = switchTop;
1150 mSwitchBottom = switchBottom;
1151 mSwitchRight = switchRight;
1152 }
1153
1154 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001155 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001156 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001157 final int switchLeft = mSwitchLeft;
1158 final int switchTop = mSwitchTop;
1159 final int switchRight = mSwitchRight;
1160 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001161
1162 int thumbInitialLeft = switchLeft + getThumbOffset();
1163
1164 final Insets thumbInsets;
1165 if (mThumbDrawable != null) {
1166 thumbInsets = mThumbDrawable.getOpticalInsets();
1167 } else {
1168 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001169 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001170
Alan Viverette0c0dde72014-07-30 13:29:39 -07001171 // Layout the track.
1172 if (mTrackDrawable != null) {
1173 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001174
Alan Viverette0c0dde72014-07-30 13:29:39 -07001175 // Adjust thumb position for track padding.
1176 thumbInitialLeft += padding.left;
1177
1178 // If necessary, offset by the optical insets of the thumb asset.
1179 int trackLeft = switchLeft;
1180 int trackTop = switchTop;
1181 int trackRight = switchRight;
1182 int trackBottom = switchBottom;
1183 if (thumbInsets != Insets.NONE) {
1184 if (thumbInsets.left > padding.left) {
1185 trackLeft += thumbInsets.left - padding.left;
1186 }
1187 if (thumbInsets.top > padding.top) {
1188 trackTop += thumbInsets.top - padding.top;
1189 }
1190 if (thumbInsets.right > padding.right) {
1191 trackRight -= thumbInsets.right - padding.right;
1192 }
1193 if (thumbInsets.bottom > padding.bottom) {
1194 trackBottom -= thumbInsets.bottom - padding.bottom;
1195 }
1196 }
1197 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1198 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001199
Alan Viverette661e6362014-05-12 10:55:37 -07001200 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001201 if (mThumbDrawable != null) {
1202 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001203
1204 final int thumbLeft = thumbInitialLeft - padding.left;
1205 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001206 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001207
Alan Viverette4d065a02014-07-11 15:28:38 -07001208 final Drawable background = getBackground();
1209 if (background != null) {
1210 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1211 }
Alan Viverette61956602014-04-22 19:07:06 -07001212 }
1213
Alan Viverettead2f8e32014-05-16 13:28:33 -07001214 // Draw the background.
1215 super.draw(c);
1216 }
1217
1218 @Override
1219 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001220 super.onDraw(canvas);
1221
Alan Viverette4d065a02014-07-11 15:28:38 -07001222 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001223 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001224 if (trackDrawable != null) {
1225 trackDrawable.getPadding(padding);
1226 } else {
1227 padding.setEmpty();
1228 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001229
1230 final int switchTop = mSwitchTop;
1231 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001232 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001233 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001234
Alan Viverette4d065a02014-07-11 15:28:38 -07001235 final Drawable thumbDrawable = mThumbDrawable;
1236 if (trackDrawable != null) {
1237 if (mSplitTrack && thumbDrawable != null) {
1238 final Insets insets = thumbDrawable.getOpticalInsets();
1239 thumbDrawable.copyBounds(padding);
1240 padding.left += insets.left;
1241 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001242
Alan Viverette4d065a02014-07-11 15:28:38 -07001243 final int saveCount = canvas.save();
1244 canvas.clipRect(padding, Op.DIFFERENCE);
1245 trackDrawable.draw(canvas);
1246 canvas.restoreToCount(saveCount);
1247 } else {
1248 trackDrawable.draw(canvas);
1249 }
Alan Viverette661e6362014-05-12 10:55:37 -07001250 }
Alan Viverette61956602014-04-22 19:07:06 -07001251
1252 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001253
1254 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001255 thumbDrawable.draw(canvas);
1256 }
Adam Powell12190b32010-11-28 19:07:53 -08001257
Alan Viverette5876ff42014-03-03 17:40:46 -08001258 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001259 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001260 final int drawableState[] = getDrawableState();
1261 if (mTextColors != null) {
1262 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1263 }
1264 mTextPaint.drawableState = drawableState;
1265
Alan Viverette4d065a02014-07-11 15:28:38 -07001266 final int cX;
1267 if (thumbDrawable != null) {
1268 final Rect bounds = thumbDrawable.getBounds();
1269 cX = bounds.left + bounds.right;
1270 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001271 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001272 }
1273
1274 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001275 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1276 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001277 switchText.draw(canvas);
1278 }
Adam Powell12190b32010-11-28 19:07:53 -08001279
Alan Viverette5876ff42014-03-03 17:40:46 -08001280 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001281 }
1282
1283 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001284 public int getCompoundPaddingLeft() {
1285 if (!isLayoutRtl()) {
1286 return super.getCompoundPaddingLeft();
1287 }
1288 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1289 if (!TextUtils.isEmpty(getText())) {
1290 padding += mSwitchPadding;
1291 }
1292 return padding;
1293 }
1294
1295 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001296 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001297 if (isLayoutRtl()) {
1298 return super.getCompoundPaddingRight();
1299 }
Adam Powell12190b32010-11-28 19:07:53 -08001300 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1301 if (!TextUtils.isEmpty(getText())) {
1302 padding += mSwitchPadding;
1303 }
1304 return padding;
1305 }
1306
Alan Viverettecc2688d2013-09-17 17:00:12 -07001307 /**
1308 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001309 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001310 *
1311 * @return thumb offset
1312 */
1313 private int getThumbOffset() {
1314 final float thumbPosition;
1315 if (isLayoutRtl()) {
1316 thumbPosition = 1 - mThumbPosition;
1317 } else {
1318 thumbPosition = mThumbPosition;
1319 }
1320 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1321 }
1322
Adam Powell12190b32010-11-28 19:07:53 -08001323 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001324 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001325 final Rect padding = mTempRect;
1326 mTrackDrawable.getPadding(padding);
1327
1328 final Insets insets;
1329 if (mThumbDrawable != null) {
1330 insets = mThumbDrawable.getOpticalInsets();
1331 } else {
1332 insets = Insets.NONE;
1333 }
1334
1335 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1336 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001337 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001338 return 0;
1339 }
Adam Powell12190b32010-11-28 19:07:53 -08001340 }
1341
1342 @Override
1343 protected int[] onCreateDrawableState(int extraSpace) {
1344 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1345 if (isChecked()) {
1346 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1347 }
1348 return drawableState;
1349 }
1350
1351 @Override
1352 protected void drawableStateChanged() {
1353 super.drawableStateChanged();
1354
Alan Viverettead0020f2015-09-04 10:10:42 -04001355 final int[] state = getDrawableState();
1356 boolean changed = false;
Adam Powell12190b32010-11-28 19:07:53 -08001357
Alan Viverettead0020f2015-09-04 10:10:42 -04001358 final Drawable thumbDrawable = mThumbDrawable;
1359 if (thumbDrawable != null && thumbDrawable.isStateful()) {
1360 changed |= thumbDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001361 }
1362
Alan Viverettead0020f2015-09-04 10:10:42 -04001363 final Drawable trackDrawable = mTrackDrawable;
1364 if (trackDrawable != null && trackDrawable.isStateful()) {
1365 changed |= trackDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001366 }
Adam Powell12190b32010-11-28 19:07:53 -08001367
Alan Viverettead0020f2015-09-04 10:10:42 -04001368 if (changed) {
1369 invalidate();
1370 }
Adam Powell12190b32010-11-28 19:07:53 -08001371 }
1372
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001373 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001374 public void drawableHotspotChanged(float x, float y) {
1375 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001376
1377 if (mThumbDrawable != null) {
1378 mThumbDrawable.setHotspot(x, y);
1379 }
1380
1381 if (mTrackDrawable != null) {
1382 mTrackDrawable.setHotspot(x, y);
1383 }
1384 }
1385
Adam Powell12190b32010-11-28 19:07:53 -08001386 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -05001387 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell12190b32010-11-28 19:07:53 -08001388 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1389 }
1390
1391 @Override
1392 public void jumpDrawablesToCurrentState() {
1393 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001394
1395 if (mThumbDrawable != null) {
1396 mThumbDrawable.jumpToCurrentState();
1397 }
1398
1399 if (mTrackDrawable != null) {
1400 mTrackDrawable.jumpToCurrentState();
1401 }
1402
Alan Viveretteee20d772015-09-23 14:23:21 -04001403 if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001404 mPositionAnimator.end();
1405 mPositionAnimator = null;
1406 }
Adam Powell12190b32010-11-28 19:07:53 -08001407 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001408
1409 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001410 public CharSequence getAccessibilityClassName() {
1411 return Switch.class.getName();
1412 }
1413
1414 @Override
Felipe Leme6d553872016-12-08 17:13:25 -08001415 public void onProvideStructure(ViewStructure structure) {
1416 super.onProvideStructure(structure);
Felipe Leme640f30a2017-03-06 15:44:06 -08001417 onProvideAutoFillStructureForAssistOrAutofill(structure);
Felipe Leme6d553872016-12-08 17:13:25 -08001418 }
Felipe Leme1ca634a2016-11-28 17:21:21 -08001419
Felipe Leme6d553872016-12-08 17:13:25 -08001420 @Override
Felipe Leme640f30a2017-03-06 15:44:06 -08001421 public void onProvideAutofillStructure(ViewStructure structure, int flags) {
1422 super.onProvideAutofillStructure(structure, flags);
1423 onProvideAutoFillStructureForAssistOrAutofill(structure);
Felipe Leme6d553872016-12-08 17:13:25 -08001424 }
1425
1426 // NOTE: currently there is no difference for Assist or AutoFill, so it doesn't take flags
Felipe Leme640f30a2017-03-06 15:44:06 -08001427 private void onProvideAutoFillStructureForAssistOrAutofill(ViewStructure structure) {
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001428 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1429 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001430 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001431 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001432 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001433 } else {
1434 StringBuilder newText = new StringBuilder();
1435 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001436 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001437 }
James Cook5cfaae42015-05-28 15:52:44 -07001438 // The style of the label text is provided via the base TextView class. This is more
1439 // relevant than the style of the (optional) on/off text on the switch button itself,
1440 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001441 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001442 }
1443
Alan Viverettea54956a2015-01-07 16:05:02 -08001444 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001445 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001446 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1447 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001448 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1449 if (!TextUtils.isEmpty(switchText)) {
1450 CharSequence oldText = info.getText();
1451 if (TextUtils.isEmpty(oldText)) {
1452 info.setText(switchText);
1453 } else {
1454 StringBuilder newText = new StringBuilder();
1455 newText.append(oldText).append(' ').append(switchText);
1456 info.setText(newText);
1457 }
1458 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001459 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001460
1461 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1462 @Override
1463 public Float get(Switch object) {
1464 return object.mThumbPosition;
1465 }
1466
1467 @Override
1468 public void setValue(Switch object, float value) {
1469 object.setThumbPosition(value);
1470 }
1471 };
Adam Powell12190b32010-11-28 19:07:53 -08001472}