blob: fcc1667d804347eb4eae2a1271a38c9d34e39d36 [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;
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;
49import android.view.ViewConfiguration;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070050import android.view.ViewStructure;
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
Siyamed Sinir79bf9d12016-05-18 19:57:52 -0700895 int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
896 transformed.length(), mTextPaint, getTextDirectionHeuristic()));
897 return new StaticLayout(transformed, mTextPaint, width,
Adam Powell12190b32010-11-28 19:07:53 -0800898 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
899 }
900
901 /**
902 * @return true if (x, y) is within the target area of the switch thumb
903 */
904 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800905 if (mThumbDrawable == null) {
906 return false;
907 }
908
Alan Viverettecc2688d2013-09-17 17:00:12 -0700909 // Relies on mTempRect, MUST be called first!
910 final int thumbOffset = getThumbOffset();
911
Adam Powell12190b32010-11-28 19:07:53 -0800912 mThumbDrawable.getPadding(mTempRect);
913 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700914 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800915 final int thumbRight = thumbLeft + mThumbWidth +
916 mTempRect.left + mTempRect.right + mTouchSlop;
917 final int thumbBottom = mSwitchBottom + mTouchSlop;
918 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
919 }
920
921 @Override
922 public boolean onTouchEvent(MotionEvent ev) {
923 mVelocityTracker.addMovement(ev);
924 final int action = ev.getActionMasked();
925 switch (action) {
926 case MotionEvent.ACTION_DOWN: {
927 final float x = ev.getX();
928 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700929 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800930 mTouchMode = TOUCH_MODE_DOWN;
931 mTouchX = x;
932 mTouchY = y;
933 }
934 break;
935 }
936
937 case MotionEvent.ACTION_MOVE: {
938 switch (mTouchMode) {
939 case TOUCH_MODE_IDLE:
940 // Didn't target the thumb, treat normally.
941 break;
942
943 case TOUCH_MODE_DOWN: {
944 final float x = ev.getX();
945 final float y = ev.getY();
946 if (Math.abs(x - mTouchX) > mTouchSlop ||
947 Math.abs(y - mTouchY) > mTouchSlop) {
948 mTouchMode = TOUCH_MODE_DRAGGING;
949 getParent().requestDisallowInterceptTouchEvent(true);
950 mTouchX = x;
951 mTouchY = y;
952 return true;
953 }
954 break;
955 }
956
957 case TOUCH_MODE_DRAGGING: {
958 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700959 final int thumbScrollRange = getThumbScrollRange();
960 final float thumbScrollOffset = x - mTouchX;
961 float dPos;
962 if (thumbScrollRange != 0) {
963 dPos = thumbScrollOffset / thumbScrollRange;
964 } else {
965 // If the thumb scroll range is empty, just use the
966 // movement direction to snap on or off.
967 dPos = thumbScrollOffset > 0 ? 1 : -1;
968 }
969 if (isLayoutRtl()) {
970 dPos = -dPos;
971 }
972 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800973 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800974 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700975 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800976 }
977 return true;
978 }
979 }
980 break;
981 }
982
983 case MotionEvent.ACTION_UP:
984 case MotionEvent.ACTION_CANCEL: {
985 if (mTouchMode == TOUCH_MODE_DRAGGING) {
986 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700987 // Allow super class to handle pressed state, etc.
988 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800989 return true;
990 }
991 mTouchMode = TOUCH_MODE_IDLE;
992 mVelocityTracker.clear();
993 break;
994 }
995 }
996
997 return super.onTouchEvent(ev);
998 }
999
1000 private void cancelSuperTouch(MotionEvent ev) {
1001 MotionEvent cancel = MotionEvent.obtain(ev);
1002 cancel.setAction(MotionEvent.ACTION_CANCEL);
1003 super.onTouchEvent(cancel);
1004 cancel.recycle();
1005 }
1006
1007 /**
1008 * Called from onTouchEvent to end a drag operation.
1009 *
1010 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1011 */
1012 private void stopDrag(MotionEvent ev) {
1013 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001014
Alan Viverette86453ff2013-09-26 14:46:08 -07001015 // Commit the change if the event is up and not canceled and the switch
1016 // has not been disabled during the drag.
1017 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001018 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001019 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001020 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001021 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001022 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001023 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001024 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001025 } else {
1026 newState = getTargetCheckedState();
1027 }
Adam Powell12190b32010-11-28 19:07:53 -08001028 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001029 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001030 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001031
Alan Viveretted4e77902014-10-27 17:50:51 -07001032 if (newState != oldState) {
1033 playSoundEffect(SoundEffectConstants.CLICK);
Alan Viveretted4e77902014-10-27 17:50:51 -07001034 }
Alan Viverette03306a02015-08-07 11:34:25 -04001035 // Always call setChecked so that the thumb is moved back to the correct edge
1036 setChecked(newState);
Alan Viverette86453ff2013-09-26 14:46:08 -07001037 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001038 }
1039
1040 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001041 final float targetPosition = newCheckedState ? 1 : 0;
1042 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1043 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1044 mPositionAnimator.setAutoCancel(true);
1045 mPositionAnimator.start();
1046 }
1047
1048 private void cancelPositionAnimator() {
1049 if (mPositionAnimator != null) {
1050 mPositionAnimator.cancel();
1051 }
Adam Powell12190b32010-11-28 19:07:53 -08001052 }
1053
1054 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001055 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001056 }
1057
Alan Viverettecc2688d2013-09-17 17:00:12 -07001058 /**
1059 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1060 *
1061 * @param position new position between [0,1]
1062 */
1063 private void setThumbPosition(float position) {
1064 mThumbPosition = position;
1065 invalidate();
1066 }
1067
1068 @Override
1069 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001070 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001071 }
1072
1073 @Override
1074 public void setChecked(boolean checked) {
1075 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001076
Alan Viverette467d6292014-08-12 15:13:19 -07001077 // Calling the super method may result in setChecked() getting called
1078 // recursively with a different value, so load the REAL value...
1079 checked = isChecked();
1080
Alan Viverette86453ff2013-09-26 14:46:08 -07001081 if (isAttachedToWindow() && isLaidOut()) {
1082 animateThumbToCheckedState(checked);
1083 } else {
1084 // Immediately move the thumb to the new position.
1085 cancelPositionAnimator();
1086 setThumbPosition(checked ? 1 : 0);
1087 }
Adam Powell12190b32010-11-28 19:07:53 -08001088 }
1089
1090 @Override
1091 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1092 super.onLayout(changed, left, top, right, bottom);
1093
Alan Viverette0c0dde72014-07-30 13:29:39 -07001094 int opticalInsetLeft = 0;
1095 int opticalInsetRight = 0;
1096 if (mThumbDrawable != null) {
1097 final Rect trackPadding = mTempRect;
1098 if (mTrackDrawable != null) {
1099 mTrackDrawable.getPadding(trackPadding);
1100 } else {
1101 trackPadding.setEmpty();
1102 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001103
Alan Viverette0c0dde72014-07-30 13:29:39 -07001104 final Insets insets = mThumbDrawable.getOpticalInsets();
1105 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1106 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001107 }
1108
Alan Viverette0c0dde72014-07-30 13:29:39 -07001109 final int switchRight;
1110 final int switchLeft;
1111 if (isLayoutRtl()) {
1112 switchLeft = getPaddingLeft() + opticalInsetLeft;
1113 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1114 } else {
1115 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1116 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1117 }
1118
1119 final int switchTop;
1120 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001121 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1122 default:
1123 case Gravity.TOP:
1124 switchTop = getPaddingTop();
1125 switchBottom = switchTop + mSwitchHeight;
1126 break;
1127
1128 case Gravity.CENTER_VERTICAL:
1129 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1130 mSwitchHeight / 2;
1131 switchBottom = switchTop + mSwitchHeight;
1132 break;
1133
1134 case Gravity.BOTTOM:
1135 switchBottom = getHeight() - getPaddingBottom();
1136 switchTop = switchBottom - mSwitchHeight;
1137 break;
1138 }
1139
1140 mSwitchLeft = switchLeft;
1141 mSwitchTop = switchTop;
1142 mSwitchBottom = switchBottom;
1143 mSwitchRight = switchRight;
1144 }
1145
1146 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001147 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001148 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001149 final int switchLeft = mSwitchLeft;
1150 final int switchTop = mSwitchTop;
1151 final int switchRight = mSwitchRight;
1152 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001153
1154 int thumbInitialLeft = switchLeft + getThumbOffset();
1155
1156 final Insets thumbInsets;
1157 if (mThumbDrawable != null) {
1158 thumbInsets = mThumbDrawable.getOpticalInsets();
1159 } else {
1160 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001161 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001162
Alan Viverette0c0dde72014-07-30 13:29:39 -07001163 // Layout the track.
1164 if (mTrackDrawable != null) {
1165 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001166
Alan Viverette0c0dde72014-07-30 13:29:39 -07001167 // Adjust thumb position for track padding.
1168 thumbInitialLeft += padding.left;
1169
1170 // If necessary, offset by the optical insets of the thumb asset.
1171 int trackLeft = switchLeft;
1172 int trackTop = switchTop;
1173 int trackRight = switchRight;
1174 int trackBottom = switchBottom;
1175 if (thumbInsets != Insets.NONE) {
1176 if (thumbInsets.left > padding.left) {
1177 trackLeft += thumbInsets.left - padding.left;
1178 }
1179 if (thumbInsets.top > padding.top) {
1180 trackTop += thumbInsets.top - padding.top;
1181 }
1182 if (thumbInsets.right > padding.right) {
1183 trackRight -= thumbInsets.right - padding.right;
1184 }
1185 if (thumbInsets.bottom > padding.bottom) {
1186 trackBottom -= thumbInsets.bottom - padding.bottom;
1187 }
1188 }
1189 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1190 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001191
Alan Viverette661e6362014-05-12 10:55:37 -07001192 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001193 if (mThumbDrawable != null) {
1194 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001195
1196 final int thumbLeft = thumbInitialLeft - padding.left;
1197 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001198 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001199
Alan Viverette4d065a02014-07-11 15:28:38 -07001200 final Drawable background = getBackground();
1201 if (background != null) {
1202 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1203 }
Alan Viverette61956602014-04-22 19:07:06 -07001204 }
1205
Alan Viverettead2f8e32014-05-16 13:28:33 -07001206 // Draw the background.
1207 super.draw(c);
1208 }
1209
1210 @Override
1211 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001212 super.onDraw(canvas);
1213
Alan Viverette4d065a02014-07-11 15:28:38 -07001214 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001215 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001216 if (trackDrawable != null) {
1217 trackDrawable.getPadding(padding);
1218 } else {
1219 padding.setEmpty();
1220 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001221
1222 final int switchTop = mSwitchTop;
1223 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001224 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001225 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001226
Alan Viverette4d065a02014-07-11 15:28:38 -07001227 final Drawable thumbDrawable = mThumbDrawable;
1228 if (trackDrawable != null) {
1229 if (mSplitTrack && thumbDrawable != null) {
1230 final Insets insets = thumbDrawable.getOpticalInsets();
1231 thumbDrawable.copyBounds(padding);
1232 padding.left += insets.left;
1233 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001234
Alan Viverette4d065a02014-07-11 15:28:38 -07001235 final int saveCount = canvas.save();
1236 canvas.clipRect(padding, Op.DIFFERENCE);
1237 trackDrawable.draw(canvas);
1238 canvas.restoreToCount(saveCount);
1239 } else {
1240 trackDrawable.draw(canvas);
1241 }
Alan Viverette661e6362014-05-12 10:55:37 -07001242 }
Alan Viverette61956602014-04-22 19:07:06 -07001243
1244 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001245
1246 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001247 thumbDrawable.draw(canvas);
1248 }
Adam Powell12190b32010-11-28 19:07:53 -08001249
Alan Viverette5876ff42014-03-03 17:40:46 -08001250 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001251 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001252 final int drawableState[] = getDrawableState();
1253 if (mTextColors != null) {
1254 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1255 }
1256 mTextPaint.drawableState = drawableState;
1257
Alan Viverette4d065a02014-07-11 15:28:38 -07001258 final int cX;
1259 if (thumbDrawable != null) {
1260 final Rect bounds = thumbDrawable.getBounds();
1261 cX = bounds.left + bounds.right;
1262 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001263 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001264 }
1265
1266 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001267 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1268 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001269 switchText.draw(canvas);
1270 }
Adam Powell12190b32010-11-28 19:07:53 -08001271
Alan Viverette5876ff42014-03-03 17:40:46 -08001272 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001273 }
1274
1275 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001276 public int getCompoundPaddingLeft() {
1277 if (!isLayoutRtl()) {
1278 return super.getCompoundPaddingLeft();
1279 }
1280 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1281 if (!TextUtils.isEmpty(getText())) {
1282 padding += mSwitchPadding;
1283 }
1284 return padding;
1285 }
1286
1287 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001288 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001289 if (isLayoutRtl()) {
1290 return super.getCompoundPaddingRight();
1291 }
Adam Powell12190b32010-11-28 19:07:53 -08001292 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1293 if (!TextUtils.isEmpty(getText())) {
1294 padding += mSwitchPadding;
1295 }
1296 return padding;
1297 }
1298
Alan Viverettecc2688d2013-09-17 17:00:12 -07001299 /**
1300 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001301 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001302 *
1303 * @return thumb offset
1304 */
1305 private int getThumbOffset() {
1306 final float thumbPosition;
1307 if (isLayoutRtl()) {
1308 thumbPosition = 1 - mThumbPosition;
1309 } else {
1310 thumbPosition = mThumbPosition;
1311 }
1312 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1313 }
1314
Adam Powell12190b32010-11-28 19:07:53 -08001315 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001316 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001317 final Rect padding = mTempRect;
1318 mTrackDrawable.getPadding(padding);
1319
1320 final Insets insets;
1321 if (mThumbDrawable != null) {
1322 insets = mThumbDrawable.getOpticalInsets();
1323 } else {
1324 insets = Insets.NONE;
1325 }
1326
1327 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1328 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001329 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001330 return 0;
1331 }
Adam Powell12190b32010-11-28 19:07:53 -08001332 }
1333
1334 @Override
1335 protected int[] onCreateDrawableState(int extraSpace) {
1336 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1337 if (isChecked()) {
1338 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1339 }
1340 return drawableState;
1341 }
1342
1343 @Override
1344 protected void drawableStateChanged() {
1345 super.drawableStateChanged();
1346
Alan Viverettead0020f2015-09-04 10:10:42 -04001347 final int[] state = getDrawableState();
1348 boolean changed = false;
Adam Powell12190b32010-11-28 19:07:53 -08001349
Alan Viverettead0020f2015-09-04 10:10:42 -04001350 final Drawable thumbDrawable = mThumbDrawable;
1351 if (thumbDrawable != null && thumbDrawable.isStateful()) {
1352 changed |= thumbDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001353 }
1354
Alan Viverettead0020f2015-09-04 10:10:42 -04001355 final Drawable trackDrawable = mTrackDrawable;
1356 if (trackDrawable != null && trackDrawable.isStateful()) {
1357 changed |= trackDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001358 }
Adam Powell12190b32010-11-28 19:07:53 -08001359
Alan Viverettead0020f2015-09-04 10:10:42 -04001360 if (changed) {
1361 invalidate();
1362 }
Adam Powell12190b32010-11-28 19:07:53 -08001363 }
1364
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001365 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001366 public void drawableHotspotChanged(float x, float y) {
1367 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001368
1369 if (mThumbDrawable != null) {
1370 mThumbDrawable.setHotspot(x, y);
1371 }
1372
1373 if (mTrackDrawable != null) {
1374 mTrackDrawable.setHotspot(x, y);
1375 }
1376 }
1377
Adam Powell12190b32010-11-28 19:07:53 -08001378 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -05001379 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell12190b32010-11-28 19:07:53 -08001380 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1381 }
1382
1383 @Override
1384 public void jumpDrawablesToCurrentState() {
1385 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001386
1387 if (mThumbDrawable != null) {
1388 mThumbDrawable.jumpToCurrentState();
1389 }
1390
1391 if (mTrackDrawable != null) {
1392 mTrackDrawable.jumpToCurrentState();
1393 }
1394
Alan Viveretteee20d772015-09-23 14:23:21 -04001395 if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001396 mPositionAnimator.end();
1397 mPositionAnimator = null;
1398 }
Adam Powell12190b32010-11-28 19:07:53 -08001399 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001400
1401 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001402 public CharSequence getAccessibilityClassName() {
1403 return Switch.class.getName();
1404 }
1405
1406 @Override
Dianne Hackborn49b043f2015-05-07 14:21:38 -07001407 public void onProvideStructure(ViewStructure structure) {
1408 super.onProvideStructure(structure);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001409 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1410 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001411 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001412 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001413 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001414 } else {
1415 StringBuilder newText = new StringBuilder();
1416 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001417 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001418 }
James Cook5cfaae42015-05-28 15:52:44 -07001419 // The style of the label text is provided via the base TextView class. This is more
1420 // relevant than the style of the (optional) on/off text on the switch button itself,
1421 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001422 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001423 }
1424
Alan Viverettea54956a2015-01-07 16:05:02 -08001425 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001426 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001427 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1428 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001429 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1430 if (!TextUtils.isEmpty(switchText)) {
1431 CharSequence oldText = info.getText();
1432 if (TextUtils.isEmpty(oldText)) {
1433 info.setText(switchText);
1434 } else {
1435 StringBuilder newText = new StringBuilder();
1436 newText.append(oldText).append(' ').append(switchText);
1437 info.setText(newText);
1438 }
1439 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001440 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001441
1442 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1443 @Override
1444 public Float get(Switch object) {
1445 return object.mThumbPosition;
1446 }
1447
1448 @Override
1449 public void setValue(Switch object, float value) {
1450 object.setThumbPosition(value);
1451 }
1452 };
Adam Powell12190b32010-11-28 19:07:53 -08001453}