blob: f45e7505f41b1c64d4db813aa5501b2e01063365 [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 Viverettee7eee642015-01-29 14:27:49 -080021import android.annotation.Nullable;
Tor Norbye7b9c9122013-05-30 16:48:33 -070022import android.annotation.StyleRes;
Adam Powell12190b32010-11-28 19:07:53 -080023import android.content.Context;
24import android.content.res.ColorStateList;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
Alan Viverette661e6362014-05-12 10:55:37 -070028import android.graphics.Insets;
Chris Craik6a49dde2015-05-12 10:28:14 -070029import android.graphics.Paint;
Alan Viverettee7eee642015-01-29 14:27:49 -080030import android.graphics.PorterDuff;
Adam Powell12190b32010-11-28 19:07:53 -080031import android.graphics.Rect;
32import android.graphics.Typeface;
Alan Viverette661e6362014-05-12 10:55:37 -070033import android.graphics.Region.Op;
Adam Powell12190b32010-11-28 19:07:53 -080034import android.graphics.drawable.Drawable;
35import android.text.Layout;
36import android.text.StaticLayout;
37import android.text.TextPaint;
38import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040039import android.text.method.AllCapsTransformationMethod;
40import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080041import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070042import android.util.FloatProperty;
43import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080044import android.view.Gravity;
45import android.view.MotionEvent;
Alan Viveretted4e77902014-10-27 17:50:51 -070046import android.view.SoundEffectConstants;
Adam Powell12190b32010-11-28 19:07:53 -080047import android.view.VelocityTracker;
Dianne Hackborn49b043f2015-05-07 14:21:38 -070048import android.view.ViewStructure;
Adam Powell12190b32010-11-28 19:07:53 -080049import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070050import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080051import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080052
Adam Powellbe0a4532010-11-29 17:47:48 -080053import com.android.internal.R;
54
Adam Powell12190b32010-11-28 19:07:53 -080055/**
56 * A Switch is a two-state toggle switch widget that can select between two
57 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070058 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
59 * property controls the text displayed in the label for the switch, whereas the
60 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
61 * controls the text on the thumb. Similarly, the
62 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
63 * setTypeface() methods control the typeface and style of label text, whereas the
64 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
65 * the related seSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080066 *
Scott Main4c359b72012-07-24 15:51:27 -070067 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
68 * guide.</p>
69 *
70 * @attr ref android.R.styleable#Switch_textOn
71 * @attr ref android.R.styleable#Switch_textOff
72 * @attr ref android.R.styleable#Switch_switchMinWidth
73 * @attr ref android.R.styleable#Switch_switchPadding
74 * @attr ref android.R.styleable#Switch_switchTextAppearance
75 * @attr ref android.R.styleable#Switch_thumb
76 * @attr ref android.R.styleable#Switch_thumbTextPadding
77 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080078 */
79public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070080 private static final int THUMB_ANIMATION_DURATION = 250;
81
Adam Powell12190b32010-11-28 19:07:53 -080082 private static final int TOUCH_MODE_IDLE = 0;
83 private static final int TOUCH_MODE_DOWN = 1;
84 private static final int TOUCH_MODE_DRAGGING = 2;
85
86 // Enum for the "typeface" XML parameter.
87 private static final int SANS = 1;
88 private static final int SERIF = 2;
89 private static final int MONOSPACE = 3;
90
91 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080092 private ColorStateList mThumbTintList = null;
93 private PorterDuff.Mode mThumbTintMode = null;
94 private boolean mHasThumbTint = false;
95 private boolean mHasThumbTintMode = false;
96
Adam Powell12190b32010-11-28 19:07:53 -080097 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080098 private ColorStateList mTrackTintList = null;
99 private PorterDuff.Mode mTrackTintMode = null;
100 private boolean mHasTrackTint = false;
101 private boolean mHasTrackTintMode = false;
102
Adam Powell12190b32010-11-28 19:07:53 -0800103 private int mThumbTextPadding;
104 private int mSwitchMinWidth;
105 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700106 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800107 private CharSequence mTextOn;
108 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700109 private boolean mShowText;
Adam Powell12190b32010-11-28 19:07:53 -0800110
111 private int mTouchMode;
112 private int mTouchSlop;
113 private float mTouchX;
114 private float mTouchY;
115 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
116 private int mMinFlingVelocity;
117
118 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700119
Alan Viverette0c0dde72014-07-30 13:29:39 -0700120 /**
121 * Width required to draw the switch track and thumb. Includes padding and
122 * optical bounds for both the track and thumb.
123 */
124 private int mSwitchWidth;
125
126 /**
127 * Height required to draw the switch track and thumb. Includes padding and
128 * optical bounds for both the track and thumb.
129 */
130 private int mSwitchHeight;
131
132 /**
133 * Width of the thumb's content region. Does not include padding or
134 * optical bounds.
135 */
136 private int mThumbWidth;
137
138 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800139 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700140
141 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800142 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700143
144 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800145 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700146
147 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800148 private int mSwitchBottom;
149
150 private TextPaint mTextPaint;
151 private ColorStateList mTextColors;
152 private Layout mOnLayout;
153 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400154 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700155 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800156
Adam Powellbe0a4532010-11-29 17:47:48 -0800157 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800158 private final Rect mTempRect = new Rect();
159
160 private static final int[] CHECKED_STATE_SET = {
161 R.attr.state_checked
162 };
163
164 /**
165 * Construct a new Switch with default styling.
166 *
167 * @param context The Context that will determine this widget's theming.
168 */
169 public Switch(Context context) {
170 this(context, null);
171 }
172
173 /**
174 * Construct a new Switch with default styling, overriding specific style
175 * attributes as requested.
176 *
177 * @param context The Context that will determine this widget's theming.
178 * @param attrs Specification of attributes that should deviate from default styling.
179 */
180 public Switch(Context context, AttributeSet attrs) {
181 this(context, attrs, com.android.internal.R.attr.switchStyle);
182 }
183
184 /**
185 * Construct a new Switch with a default style determined by the given theme attribute,
186 * overriding specific style attributes as requested.
187 *
188 * @param context The Context that will determine this widget's theming.
189 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700190 * @param defStyleAttr An attribute in the current theme that contains a
191 * reference to a style resource that supplies default values for
192 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800193 */
Alan Viverette617feb92013-09-09 18:09:13 -0700194 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
195 this(context, attrs, defStyleAttr, 0);
196 }
197
198
199 /**
200 * Construct a new Switch with a default style determined by the given theme
201 * attribute or style resource, overriding specific style attributes as
202 * requested.
203 *
204 * @param context The Context that will determine this widget's theming.
205 * @param attrs Specification of attributes that should deviate from the
206 * default styling.
207 * @param defStyleAttr An attribute in the current theme that contains a
208 * reference to a style resource that supplies default values for
209 * the view. Can be 0 to not look for defaults.
210 * @param defStyleRes A resource identifier of a style resource that
211 * supplies default values for the view, used only if
212 * defStyleAttr is 0 or can not be found in the theme. Can be 0
213 * to not look for defaults.
214 */
215 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
216 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800217
Chris Craik6a49dde2015-05-12 10:28:14 -0700218 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700219
220 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800221 mTextPaint.density = res.getDisplayMetrics().density;
222 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
223
Alan Viverette617feb92013-09-09 18:09:13 -0700224 final TypedArray a = context.obtainStyledAttributes(
225 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700226 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700227 if (mThumbDrawable != null) {
228 mThumbDrawable.setCallback(this);
229 }
Chet Haase150176d2011-08-26 09:54:06 -0700230 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700231 if (mTrackDrawable != null) {
232 mTrackDrawable.setCallback(this);
233 }
Adam Powell12190b32010-11-28 19:07:53 -0800234 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
235 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700236 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800237 mThumbTextPadding = a.getDimensionPixelSize(
238 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
239 mSwitchMinWidth = a.getDimensionPixelSize(
240 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
241 mSwitchPadding = a.getDimensionPixelSize(
242 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700243 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800244
Jun Mukai9fb302c2015-06-24 18:31:06 -0700245 ColorStateList thumbTintList = a.getColorStateList(
246 com.android.internal.R.styleable.Switch_thumbTint);
247 if (thumbTintList != null) {
248 mThumbTintList = thumbTintList;
249 mHasThumbTint = true;
250 }
251 PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
252 a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
253 if (mThumbTintMode != thumbTintMode) {
254 mThumbTintMode = thumbTintMode;
255 mHasThumbTintMode = true;
256 }
257 if (mHasThumbTint || mHasThumbTintMode) {
258 applyThumbTint();
259 }
260
261 ColorStateList trackTintList = a.getColorStateList(
262 com.android.internal.R.styleable.Switch_trackTint);
263 if (trackTintList != null) {
264 mTrackTintList = trackTintList;
265 mHasTrackTint = true;
266 }
267 PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
268 a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
269 if (mTrackTintMode != trackTintMode) {
270 mTrackTintMode = trackTintMode;
271 mHasTrackTintMode = true;
272 }
273 if (mHasTrackTint || mHasTrackTintMode) {
274 applyTrackTint();
275 }
276
Alan Viverette661e6362014-05-12 10:55:37 -0700277 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800278 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
279 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700280 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800281 }
282 a.recycle();
283
Alan Viverette661e6362014-05-12 10:55:37 -0700284 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800285 mTouchSlop = config.getScaledTouchSlop();
286 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
287
288 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700289 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800290 setChecked(isChecked());
291 }
292
293 /**
294 * Sets the switch text color, size, style, hint color, and highlight color
295 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800296 *
297 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800298 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700299 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800300 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700301 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800302 com.android.internal.R.styleable.TextAppearance);
303
304 ColorStateList colors;
305 int ts;
306
307 colors = appearance.getColorStateList(com.android.internal.R.styleable.
308 TextAppearance_textColor);
309 if (colors != null) {
310 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700311 } else {
312 // If no color set in TextAppearance, default to the view's textColor
313 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800314 }
315
316 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
317 TextAppearance_textSize, 0);
318 if (ts != 0) {
319 if (ts != mTextPaint.getTextSize()) {
320 mTextPaint.setTextSize(ts);
321 requestLayout();
322 }
323 }
324
325 int typefaceIndex, styleIndex;
326
327 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
328 TextAppearance_typeface, -1);
329 styleIndex = appearance.getInt(com.android.internal.R.styleable.
330 TextAppearance_textStyle, -1);
331
332 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
333
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400334 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
335 TextAppearance_textAllCaps, false);
336 if (allCaps) {
337 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
338 mSwitchTransformationMethod.setLengthChangesAllowed(true);
339 } else {
340 mSwitchTransformationMethod = null;
341 }
342
Adam Powell12190b32010-11-28 19:07:53 -0800343 appearance.recycle();
344 }
345
346 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
347 Typeface tf = null;
348 switch (typefaceIndex) {
349 case SANS:
350 tf = Typeface.SANS_SERIF;
351 break;
352
353 case SERIF:
354 tf = Typeface.SERIF;
355 break;
356
357 case MONOSPACE:
358 tf = Typeface.MONOSPACE;
359 break;
360 }
361
362 setSwitchTypeface(tf, styleIndex);
363 }
364
365 /**
366 * Sets the typeface and style in which the text should be displayed on the
367 * switch, and turns on the fake bold and italic bits in the Paint if the
368 * Typeface that you provided does not have all the bits in the
369 * style that you specified.
370 */
371 public void setSwitchTypeface(Typeface tf, int style) {
372 if (style > 0) {
373 if (tf == null) {
374 tf = Typeface.defaultFromStyle(style);
375 } else {
376 tf = Typeface.create(tf, style);
377 }
378
379 setSwitchTypeface(tf);
380 // now compute what (if any) algorithmic styling is needed
381 int typefaceStyle = tf != null ? tf.getStyle() : 0;
382 int need = style & ~typefaceStyle;
383 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
384 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
385 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700386 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800387 mTextPaint.setTextSkewX(0);
388 setSwitchTypeface(tf);
389 }
390 }
391
392 /**
Chet Haase150176d2011-08-26 09:54:06 -0700393 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800394 * Note that not all Typeface families actually have bold and italic
395 * variants, so you may need to use
396 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
397 * that you actually want.
398 *
399 * @attr ref android.R.styleable#TextView_typeface
400 * @attr ref android.R.styleable#TextView_textStyle
401 */
402 public void setSwitchTypeface(Typeface tf) {
403 if (mTextPaint.getTypeface() != tf) {
404 mTextPaint.setTypeface(tf);
405
406 requestLayout();
407 invalidate();
408 }
409 }
410
411 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800412 * Set the amount of horizontal padding between the switch and the associated text.
413 *
414 * @param pixels Amount of padding in pixels
415 *
416 * @attr ref android.R.styleable#Switch_switchPadding
417 */
418 public void setSwitchPadding(int pixels) {
419 mSwitchPadding = pixels;
420 requestLayout();
421 }
422
423 /**
424 * Get the amount of horizontal padding between the switch and the associated text.
425 *
426 * @return Amount of padding in pixels
427 *
428 * @attr ref android.R.styleable#Switch_switchPadding
429 */
430 public int getSwitchPadding() {
431 return mSwitchPadding;
432 }
433
434 /**
435 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
436 * of this value and its measured width as determined by the switch drawables and text used.
437 *
438 * @param pixels Minimum width of the switch in pixels
439 *
440 * @attr ref android.R.styleable#Switch_switchMinWidth
441 */
442 public void setSwitchMinWidth(int pixels) {
443 mSwitchMinWidth = pixels;
444 requestLayout();
445 }
446
447 /**
448 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
449 * of this value and its measured width as determined by the switch drawables and text used.
450 *
451 * @return Minimum width of the switch in pixels
452 *
453 * @attr ref android.R.styleable#Switch_switchMinWidth
454 */
455 public int getSwitchMinWidth() {
456 return mSwitchMinWidth;
457 }
458
459 /**
460 * Set the horizontal padding around the text drawn on the switch itself.
461 *
462 * @param pixels Horizontal padding for switch thumb text in pixels
463 *
464 * @attr ref android.R.styleable#Switch_thumbTextPadding
465 */
466 public void setThumbTextPadding(int pixels) {
467 mThumbTextPadding = pixels;
468 requestLayout();
469 }
470
471 /**
472 * Get the horizontal padding around the text drawn on the switch itself.
473 *
474 * @return Horizontal padding for switch thumb text in pixels
475 *
476 * @attr ref android.R.styleable#Switch_thumbTextPadding
477 */
478 public int getThumbTextPadding() {
479 return mThumbTextPadding;
480 }
481
482 /**
483 * Set the drawable used for the track that the switch slides within.
484 *
485 * @param track Track drawable
486 *
487 * @attr ref android.R.styleable#Switch_track
488 */
489 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700490 if (mTrackDrawable != null) {
491 mTrackDrawable.setCallback(null);
492 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800493 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700494 if (track != null) {
495 track.setCallback(this);
496 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800497 requestLayout();
498 }
499
500 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800501 * Set the drawable used for the track that the switch slides within.
502 *
Adam Powelldca510e2012-03-08 20:06:39 -0800503 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800504 *
505 * @attr ref android.R.styleable#Switch_track
506 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700507 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800508 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800509 }
510
511 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800512 * Get the drawable used for the track that the switch slides within.
513 *
514 * @return Track drawable
515 *
516 * @attr ref android.R.styleable#Switch_track
517 */
518 public Drawable getTrackDrawable() {
519 return mTrackDrawable;
520 }
521
522 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800523 * Applies a tint to the track drawable. Does not modify the current
524 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
525 * <p>
526 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
527 * automatically mutate the drawable and apply the specified tint and tint
528 * mode using {@link Drawable#setTintList(ColorStateList)}.
529 *
530 * @param tint the tint to apply, may be {@code null} to clear tint
531 *
532 * @attr ref android.R.styleable#Switch_trackTint
533 * @see #getTrackTintList()
534 * @see Drawable#setTintList(ColorStateList)
535 */
536 public void setTrackTintList(@Nullable ColorStateList tint) {
537 mTrackTintList = tint;
538 mHasTrackTint = true;
539
540 applyTrackTint();
541 }
542
543 /**
544 * @return the tint applied to the track drawable
545 * @attr ref android.R.styleable#Switch_trackTint
546 * @see #setTrackTintList(ColorStateList)
547 */
548 @Nullable
549 public ColorStateList getTrackTintList() {
550 return mTrackTintList;
551 }
552
553 /**
554 * Specifies the blending mode used to apply the tint specified by
555 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
556 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
557 *
558 * @param tintMode the blending mode used to apply the tint, may be
559 * {@code null} to clear tint
560 * @attr ref android.R.styleable#Switch_trackTintMode
561 * @see #getTrackTintMode()
562 * @see Drawable#setTintMode(PorterDuff.Mode)
563 */
564 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
565 mTrackTintMode = tintMode;
566 mHasTrackTintMode = true;
567
568 applyTrackTint();
569 }
570
571 /**
572 * @return the blending mode used to apply the tint to the track
573 * drawable
574 * @attr ref android.R.styleable#Switch_trackTintMode
575 * @see #setTrackTintMode(PorterDuff.Mode)
576 */
577 @Nullable
578 public PorterDuff.Mode getTrackTintMode() {
579 return mTrackTintMode;
580 }
581
582 private void applyTrackTint() {
583 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
584 mTrackDrawable = mTrackDrawable.mutate();
585
586 if (mHasTrackTint) {
587 mTrackDrawable.setTintList(mTrackTintList);
588 }
589
590 if (mHasTrackTintMode) {
591 mTrackDrawable.setTintMode(mTrackTintMode);
592 }
593
594 // The drawable (or one of its children) may not have been
595 // stateful before applying the tint, so let's try again.
596 if (mTrackDrawable.isStateful()) {
597 mTrackDrawable.setState(getDrawableState());
598 }
599 }
600 }
601
602 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800603 * Set the drawable used for the switch "thumb" - the piece that the user
604 * can physically touch and drag along the track.
605 *
606 * @param thumb Thumb drawable
607 *
608 * @attr ref android.R.styleable#Switch_thumb
609 */
610 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700611 if (mThumbDrawable != null) {
612 mThumbDrawable.setCallback(null);
613 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800614 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700615 if (thumb != null) {
616 thumb.setCallback(this);
617 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800618 requestLayout();
619 }
620
621 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800622 * Set the drawable used for the switch "thumb" - the piece that the user
623 * can physically touch and drag along the track.
624 *
Adam Powelldca510e2012-03-08 20:06:39 -0800625 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800626 *
627 * @attr ref android.R.styleable#Switch_thumb
628 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700629 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800630 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800631 }
632
633 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800634 * Get the drawable used for the switch "thumb" - the piece that the user
635 * can physically touch and drag along the track.
636 *
637 * @return Thumb drawable
638 *
639 * @attr ref android.R.styleable#Switch_thumb
640 */
641 public Drawable getThumbDrawable() {
642 return mThumbDrawable;
643 }
644
645 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800646 * Applies a tint to the thumb drawable. Does not modify the current
647 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
648 * <p>
649 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
650 * automatically mutate the drawable and apply the specified tint and tint
651 * mode using {@link Drawable#setTintList(ColorStateList)}.
652 *
653 * @param tint the tint to apply, may be {@code null} to clear tint
654 *
655 * @attr ref android.R.styleable#Switch_thumbTint
656 * @see #getThumbTintList()
657 * @see Drawable#setTintList(ColorStateList)
658 */
659 public void setThumbTintList(@Nullable ColorStateList tint) {
660 mThumbTintList = tint;
661 mHasThumbTint = true;
662
663 applyThumbTint();
664 }
665
666 /**
667 * @return the tint applied to the thumb drawable
668 * @attr ref android.R.styleable#Switch_thumbTint
669 * @see #setThumbTintList(ColorStateList)
670 */
671 @Nullable
672 public ColorStateList getThumbTintList() {
673 return mThumbTintList;
674 }
675
676 /**
677 * Specifies the blending mode used to apply the tint specified by
678 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
679 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
680 *
681 * @param tintMode the blending mode used to apply the tint, may be
682 * {@code null} to clear tint
683 * @attr ref android.R.styleable#Switch_thumbTintMode
684 * @see #getThumbTintMode()
685 * @see Drawable#setTintMode(PorterDuff.Mode)
686 */
687 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
688 mThumbTintMode = tintMode;
689 mHasThumbTintMode = true;
690
691 applyThumbTint();
692 }
693
694 /**
695 * @return the blending mode used to apply the tint to the thumb
696 * drawable
697 * @attr ref android.R.styleable#Switch_thumbTintMode
698 * @see #setThumbTintMode(PorterDuff.Mode)
699 */
700 @Nullable
701 public PorterDuff.Mode getThumbTintMode() {
702 return mThumbTintMode;
703 }
704
705 private void applyThumbTint() {
706 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
707 mThumbDrawable = mThumbDrawable.mutate();
708
709 if (mHasThumbTint) {
710 mThumbDrawable.setTintList(mThumbTintList);
711 }
712
713 if (mHasThumbTintMode) {
714 mThumbDrawable.setTintMode(mThumbTintMode);
715 }
716
717 // The drawable (or one of its children) may not have been
718 // stateful before applying the tint, so let's try again.
719 if (mThumbDrawable.isStateful()) {
720 mThumbDrawable.setState(getDrawableState());
721 }
722 }
723 }
724
725 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700726 * Specifies whether the track should be split by the thumb. When true,
727 * the thumb's optical bounds will be clipped out of the track drawable,
728 * then the thumb will be drawn into the resulting gap.
729 *
730 * @param splitTrack Whether the track should be split by the thumb
731 *
732 * @attr ref android.R.styleable#Switch_splitTrack
733 */
734 public void setSplitTrack(boolean splitTrack) {
735 mSplitTrack = splitTrack;
736 invalidate();
737 }
738
739 /**
740 * Returns whether the track should be split by the thumb.
741 *
742 * @attr ref android.R.styleable#Switch_splitTrack
743 */
744 public boolean getSplitTrack() {
745 return mSplitTrack;
746 }
747
748 /**
Chet Haase150176d2011-08-26 09:54:06 -0700749 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800750 *
751 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800752 */
753 public CharSequence getTextOn() {
754 return mTextOn;
755 }
756
757 /**
Chet Haase150176d2011-08-26 09:54:06 -0700758 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800759 *
760 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800761 */
762 public void setTextOn(CharSequence textOn) {
763 mTextOn = textOn;
764 requestLayout();
765 }
766
767 /**
Chet Haase150176d2011-08-26 09:54:06 -0700768 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800769 *
770 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800771 */
772 public CharSequence getTextOff() {
773 return mTextOff;
774 }
775
776 /**
Chet Haase150176d2011-08-26 09:54:06 -0700777 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800778 *
779 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800780 */
781 public void setTextOff(CharSequence textOff) {
782 mTextOff = textOff;
783 requestLayout();
784 }
785
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700786 /**
787 * Sets whether the on/off text should be displayed.
788 *
789 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700790 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700791 */
792 public void setShowText(boolean showText) {
793 if (mShowText != showText) {
794 mShowText = showText;
795 requestLayout();
796 }
797 }
798
799 /**
800 * @return whether the on/off text should be displayed
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 boolean getShowText() {
804 return mShowText;
805 }
806
Adam Powell12190b32010-11-28 19:07:53 -0800807 @Override
808 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700809 if (mShowText) {
810 if (mOnLayout == null) {
811 mOnLayout = makeLayout(mTextOn);
812 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800813
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700814 if (mOffLayout == null) {
815 mOffLayout = makeLayout(mTextOff);
816 }
Adam Powell12190b32010-11-28 19:07:53 -0800817 }
818
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000819 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700820 final int thumbWidth;
821 final int thumbHeight;
822 if (mThumbDrawable != null) {
823 // Cached thumb width does not include padding.
824 mThumbDrawable.getPadding(padding);
825 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
826 thumbHeight = mThumbDrawable.getIntrinsicHeight();
827 } else {
828 thumbWidth = 0;
829 thumbHeight = 0;
830 }
831
832 final int maxTextWidth;
833 if (mShowText) {
834 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
835 + mThumbTextPadding * 2;
836 } else {
837 maxTextWidth = 0;
838 }
839
840 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
841
842 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700843 if (mTrackDrawable != null) {
844 mTrackDrawable.getPadding(padding);
845 trackHeight = mTrackDrawable.getIntrinsicHeight();
846 } else {
847 padding.setEmpty();
848 trackHeight = 0;
849 }
850
Alan Viverette0c0dde72014-07-30 13:29:39 -0700851 // Adjust left and right padding to ensure there's enough room for the
852 // thumb's padding (when present).
853 int paddingLeft = padding.left;
854 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700855 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700856 final Insets inset = mThumbDrawable.getOpticalInsets();
857 paddingLeft = Math.max(paddingLeft, inset.left);
858 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700859 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800860
Adam Powell12190b32010-11-28 19:07:53 -0800861 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700862 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700863 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800864 mSwitchWidth = switchWidth;
865 mSwitchHeight = switchHeight;
866
867 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700868
Adam Powell12190b32010-11-28 19:07:53 -0800869 final int measuredHeight = getMeasuredHeight();
870 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800871 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800872 }
873 }
874
Alan Viverettea54956a2015-01-07 16:05:02 -0800875 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700876 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800877 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
878 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700879
880 final CharSequence text = isChecked() ? mTextOn : mTextOff;
881 if (text != null) {
882 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700883 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700884 }
885
Adam Powell12190b32010-11-28 19:07:53 -0800886 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400887 final CharSequence transformed = (mSwitchTransformationMethod != null)
888 ? mSwitchTransformationMethod.getTransformation(text, this)
889 : text;
890
891 return new StaticLayout(transformed, mTextPaint,
892 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800893 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
894 }
895
896 /**
897 * @return true if (x, y) is within the target area of the switch thumb
898 */
899 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800900 if (mThumbDrawable == null) {
901 return false;
902 }
903
Alan Viverettecc2688d2013-09-17 17:00:12 -0700904 // Relies on mTempRect, MUST be called first!
905 final int thumbOffset = getThumbOffset();
906
Adam Powell12190b32010-11-28 19:07:53 -0800907 mThumbDrawable.getPadding(mTempRect);
908 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700909 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800910 final int thumbRight = thumbLeft + mThumbWidth +
911 mTempRect.left + mTempRect.right + mTouchSlop;
912 final int thumbBottom = mSwitchBottom + mTouchSlop;
913 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
914 }
915
916 @Override
917 public boolean onTouchEvent(MotionEvent ev) {
918 mVelocityTracker.addMovement(ev);
919 final int action = ev.getActionMasked();
920 switch (action) {
921 case MotionEvent.ACTION_DOWN: {
922 final float x = ev.getX();
923 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700924 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800925 mTouchMode = TOUCH_MODE_DOWN;
926 mTouchX = x;
927 mTouchY = y;
928 }
929 break;
930 }
931
932 case MotionEvent.ACTION_MOVE: {
933 switch (mTouchMode) {
934 case TOUCH_MODE_IDLE:
935 // Didn't target the thumb, treat normally.
936 break;
937
938 case TOUCH_MODE_DOWN: {
939 final float x = ev.getX();
940 final float y = ev.getY();
941 if (Math.abs(x - mTouchX) > mTouchSlop ||
942 Math.abs(y - mTouchY) > mTouchSlop) {
943 mTouchMode = TOUCH_MODE_DRAGGING;
944 getParent().requestDisallowInterceptTouchEvent(true);
945 mTouchX = x;
946 mTouchY = y;
947 return true;
948 }
949 break;
950 }
951
952 case TOUCH_MODE_DRAGGING: {
953 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700954 final int thumbScrollRange = getThumbScrollRange();
955 final float thumbScrollOffset = x - mTouchX;
956 float dPos;
957 if (thumbScrollRange != 0) {
958 dPos = thumbScrollOffset / thumbScrollRange;
959 } else {
960 // If the thumb scroll range is empty, just use the
961 // movement direction to snap on or off.
962 dPos = thumbScrollOffset > 0 ? 1 : -1;
963 }
964 if (isLayoutRtl()) {
965 dPos = -dPos;
966 }
967 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800968 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800969 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700970 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800971 }
972 return true;
973 }
974 }
975 break;
976 }
977
978 case MotionEvent.ACTION_UP:
979 case MotionEvent.ACTION_CANCEL: {
980 if (mTouchMode == TOUCH_MODE_DRAGGING) {
981 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700982 // Allow super class to handle pressed state, etc.
983 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800984 return true;
985 }
986 mTouchMode = TOUCH_MODE_IDLE;
987 mVelocityTracker.clear();
988 break;
989 }
990 }
991
992 return super.onTouchEvent(ev);
993 }
994
995 private void cancelSuperTouch(MotionEvent ev) {
996 MotionEvent cancel = MotionEvent.obtain(ev);
997 cancel.setAction(MotionEvent.ACTION_CANCEL);
998 super.onTouchEvent(cancel);
999 cancel.recycle();
1000 }
1001
1002 /**
1003 * Called from onTouchEvent to end a drag operation.
1004 *
1005 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1006 */
1007 private void stopDrag(MotionEvent ev) {
1008 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001009
Alan Viverette86453ff2013-09-26 14:46:08 -07001010 // Commit the change if the event is up and not canceled and the switch
1011 // has not been disabled during the drag.
1012 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001013 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001014 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001015 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001016 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001017 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001018 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001019 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001020 } else {
1021 newState = getTargetCheckedState();
1022 }
Adam Powell12190b32010-11-28 19:07:53 -08001023 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001024 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001025 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001026
Alan Viveretted4e77902014-10-27 17:50:51 -07001027 if (newState != oldState) {
1028 playSoundEffect(SoundEffectConstants.CLICK);
1029 setChecked(newState);
1030 }
1031
Alan Viverette86453ff2013-09-26 14:46:08 -07001032 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001033 }
1034
1035 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001036 final float targetPosition = newCheckedState ? 1 : 0;
1037 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1038 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1039 mPositionAnimator.setAutoCancel(true);
1040 mPositionAnimator.start();
1041 }
1042
1043 private void cancelPositionAnimator() {
1044 if (mPositionAnimator != null) {
1045 mPositionAnimator.cancel();
1046 }
Adam Powell12190b32010-11-28 19:07:53 -08001047 }
1048
1049 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001050 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001051 }
1052
Alan Viverettecc2688d2013-09-17 17:00:12 -07001053 /**
1054 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1055 *
1056 * @param position new position between [0,1]
1057 */
1058 private void setThumbPosition(float position) {
1059 mThumbPosition = position;
1060 invalidate();
1061 }
1062
1063 @Override
1064 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001065 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001066 }
1067
1068 @Override
1069 public void setChecked(boolean checked) {
1070 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001071
Alan Viverette467d6292014-08-12 15:13:19 -07001072 // Calling the super method may result in setChecked() getting called
1073 // recursively with a different value, so load the REAL value...
1074 checked = isChecked();
1075
Alan Viverette86453ff2013-09-26 14:46:08 -07001076 if (isAttachedToWindow() && isLaidOut()) {
1077 animateThumbToCheckedState(checked);
1078 } else {
1079 // Immediately move the thumb to the new position.
1080 cancelPositionAnimator();
1081 setThumbPosition(checked ? 1 : 0);
1082 }
Adam Powell12190b32010-11-28 19:07:53 -08001083 }
1084
1085 @Override
1086 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1087 super.onLayout(changed, left, top, right, bottom);
1088
Alan Viverette0c0dde72014-07-30 13:29:39 -07001089 int opticalInsetLeft = 0;
1090 int opticalInsetRight = 0;
1091 if (mThumbDrawable != null) {
1092 final Rect trackPadding = mTempRect;
1093 if (mTrackDrawable != null) {
1094 mTrackDrawable.getPadding(trackPadding);
1095 } else {
1096 trackPadding.setEmpty();
1097 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001098
Alan Viverette0c0dde72014-07-30 13:29:39 -07001099 final Insets insets = mThumbDrawable.getOpticalInsets();
1100 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1101 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001102 }
1103
Alan Viverette0c0dde72014-07-30 13:29:39 -07001104 final int switchRight;
1105 final int switchLeft;
1106 if (isLayoutRtl()) {
1107 switchLeft = getPaddingLeft() + opticalInsetLeft;
1108 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1109 } else {
1110 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1111 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1112 }
1113
1114 final int switchTop;
1115 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001116 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1117 default:
1118 case Gravity.TOP:
1119 switchTop = getPaddingTop();
1120 switchBottom = switchTop + mSwitchHeight;
1121 break;
1122
1123 case Gravity.CENTER_VERTICAL:
1124 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1125 mSwitchHeight / 2;
1126 switchBottom = switchTop + mSwitchHeight;
1127 break;
1128
1129 case Gravity.BOTTOM:
1130 switchBottom = getHeight() - getPaddingBottom();
1131 switchTop = switchBottom - mSwitchHeight;
1132 break;
1133 }
1134
1135 mSwitchLeft = switchLeft;
1136 mSwitchTop = switchTop;
1137 mSwitchBottom = switchBottom;
1138 mSwitchRight = switchRight;
1139 }
1140
1141 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001142 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001143 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001144 final int switchLeft = mSwitchLeft;
1145 final int switchTop = mSwitchTop;
1146 final int switchRight = mSwitchRight;
1147 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001148
1149 int thumbInitialLeft = switchLeft + getThumbOffset();
1150
1151 final Insets thumbInsets;
1152 if (mThumbDrawable != null) {
1153 thumbInsets = mThumbDrawable.getOpticalInsets();
1154 } else {
1155 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001156 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001157
Alan Viverette0c0dde72014-07-30 13:29:39 -07001158 // Layout the track.
1159 if (mTrackDrawable != null) {
1160 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001161
Alan Viverette0c0dde72014-07-30 13:29:39 -07001162 // Adjust thumb position for track padding.
1163 thumbInitialLeft += padding.left;
1164
1165 // If necessary, offset by the optical insets of the thumb asset.
1166 int trackLeft = switchLeft;
1167 int trackTop = switchTop;
1168 int trackRight = switchRight;
1169 int trackBottom = switchBottom;
1170 if (thumbInsets != Insets.NONE) {
1171 if (thumbInsets.left > padding.left) {
1172 trackLeft += thumbInsets.left - padding.left;
1173 }
1174 if (thumbInsets.top > padding.top) {
1175 trackTop += thumbInsets.top - padding.top;
1176 }
1177 if (thumbInsets.right > padding.right) {
1178 trackRight -= thumbInsets.right - padding.right;
1179 }
1180 if (thumbInsets.bottom > padding.bottom) {
1181 trackBottom -= thumbInsets.bottom - padding.bottom;
1182 }
1183 }
1184 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1185 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001186
Alan Viverette661e6362014-05-12 10:55:37 -07001187 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001188 if (mThumbDrawable != null) {
1189 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001190
1191 final int thumbLeft = thumbInitialLeft - padding.left;
1192 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001193 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001194
Alan Viverette4d065a02014-07-11 15:28:38 -07001195 final Drawable background = getBackground();
1196 if (background != null) {
1197 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1198 }
Alan Viverette61956602014-04-22 19:07:06 -07001199 }
1200
Alan Viverettead2f8e32014-05-16 13:28:33 -07001201 // Draw the background.
1202 super.draw(c);
1203 }
1204
1205 @Override
1206 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001207 super.onDraw(canvas);
1208
Alan Viverette4d065a02014-07-11 15:28:38 -07001209 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001210 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001211 if (trackDrawable != null) {
1212 trackDrawable.getPadding(padding);
1213 } else {
1214 padding.setEmpty();
1215 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001216
1217 final int switchTop = mSwitchTop;
1218 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001219 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001220 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001221
Alan Viverette4d065a02014-07-11 15:28:38 -07001222 final Drawable thumbDrawable = mThumbDrawable;
1223 if (trackDrawable != null) {
1224 if (mSplitTrack && thumbDrawable != null) {
1225 final Insets insets = thumbDrawable.getOpticalInsets();
1226 thumbDrawable.copyBounds(padding);
1227 padding.left += insets.left;
1228 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001229
Alan Viverette4d065a02014-07-11 15:28:38 -07001230 final int saveCount = canvas.save();
1231 canvas.clipRect(padding, Op.DIFFERENCE);
1232 trackDrawable.draw(canvas);
1233 canvas.restoreToCount(saveCount);
1234 } else {
1235 trackDrawable.draw(canvas);
1236 }
Alan Viverette661e6362014-05-12 10:55:37 -07001237 }
Alan Viverette61956602014-04-22 19:07:06 -07001238
1239 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001240
1241 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001242 thumbDrawable.draw(canvas);
1243 }
Adam Powell12190b32010-11-28 19:07:53 -08001244
Alan Viverette5876ff42014-03-03 17:40:46 -08001245 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001246 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001247 final int drawableState[] = getDrawableState();
1248 if (mTextColors != null) {
1249 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1250 }
1251 mTextPaint.drawableState = drawableState;
1252
Alan Viverette4d065a02014-07-11 15:28:38 -07001253 final int cX;
1254 if (thumbDrawable != null) {
1255 final Rect bounds = thumbDrawable.getBounds();
1256 cX = bounds.left + bounds.right;
1257 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001258 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001259 }
1260
1261 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001262 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1263 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001264 switchText.draw(canvas);
1265 }
Adam Powell12190b32010-11-28 19:07:53 -08001266
Alan Viverette5876ff42014-03-03 17:40:46 -08001267 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001268 }
1269
1270 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001271 public int getCompoundPaddingLeft() {
1272 if (!isLayoutRtl()) {
1273 return super.getCompoundPaddingLeft();
1274 }
1275 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1276 if (!TextUtils.isEmpty(getText())) {
1277 padding += mSwitchPadding;
1278 }
1279 return padding;
1280 }
1281
1282 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001283 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001284 if (isLayoutRtl()) {
1285 return super.getCompoundPaddingRight();
1286 }
Adam Powell12190b32010-11-28 19:07:53 -08001287 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1288 if (!TextUtils.isEmpty(getText())) {
1289 padding += mSwitchPadding;
1290 }
1291 return padding;
1292 }
1293
Alan Viverettecc2688d2013-09-17 17:00:12 -07001294 /**
1295 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001296 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001297 *
1298 * @return thumb offset
1299 */
1300 private int getThumbOffset() {
1301 final float thumbPosition;
1302 if (isLayoutRtl()) {
1303 thumbPosition = 1 - mThumbPosition;
1304 } else {
1305 thumbPosition = mThumbPosition;
1306 }
1307 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1308 }
1309
Adam Powell12190b32010-11-28 19:07:53 -08001310 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001311 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001312 final Rect padding = mTempRect;
1313 mTrackDrawable.getPadding(padding);
1314
1315 final Insets insets;
1316 if (mThumbDrawable != null) {
1317 insets = mThumbDrawable.getOpticalInsets();
1318 } else {
1319 insets = Insets.NONE;
1320 }
1321
1322 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1323 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001324 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001325 return 0;
1326 }
Adam Powell12190b32010-11-28 19:07:53 -08001327 }
1328
1329 @Override
1330 protected int[] onCreateDrawableState(int extraSpace) {
1331 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1332 if (isChecked()) {
1333 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1334 }
1335 return drawableState;
1336 }
1337
1338 @Override
1339 protected void drawableStateChanged() {
1340 super.drawableStateChanged();
1341
Alan Viverette661e6362014-05-12 10:55:37 -07001342 final int[] myDrawableState = getDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -08001343
Alan Viverette2356c5e2014-05-22 22:43:59 -07001344 if (mThumbDrawable != null) {
1345 mThumbDrawable.setState(myDrawableState);
Alan Viverette661e6362014-05-12 10:55:37 -07001346 }
1347
1348 if (mTrackDrawable != null) {
1349 mTrackDrawable.setState(myDrawableState);
1350 }
Adam Powell12190b32010-11-28 19:07:53 -08001351
1352 invalidate();
1353 }
1354
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001355 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001356 public void drawableHotspotChanged(float x, float y) {
1357 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001358
1359 if (mThumbDrawable != null) {
1360 mThumbDrawable.setHotspot(x, y);
1361 }
1362
1363 if (mTrackDrawable != null) {
1364 mTrackDrawable.setHotspot(x, y);
1365 }
1366 }
1367
Adam Powell12190b32010-11-28 19:07:53 -08001368 @Override
1369 protected boolean verifyDrawable(Drawable who) {
1370 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1371 }
1372
1373 @Override
1374 public void jumpDrawablesToCurrentState() {
1375 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001376
1377 if (mThumbDrawable != null) {
1378 mThumbDrawable.jumpToCurrentState();
1379 }
1380
1381 if (mTrackDrawable != null) {
1382 mTrackDrawable.jumpToCurrentState();
1383 }
1384
1385 if (mPositionAnimator != null && mPositionAnimator.isRunning()) {
1386 mPositionAnimator.end();
1387 mPositionAnimator = null;
1388 }
Adam Powell12190b32010-11-28 19:07:53 -08001389 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001390
1391 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001392 public CharSequence getAccessibilityClassName() {
1393 return Switch.class.getName();
1394 }
1395
1396 @Override
Dianne Hackborn49b043f2015-05-07 14:21:38 -07001397 public void onProvideStructure(ViewStructure structure) {
1398 super.onProvideStructure(structure);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001399 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1400 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001401 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001402 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001403 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001404 } else {
1405 StringBuilder newText = new StringBuilder();
1406 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001407 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001408 }
James Cook5cfaae42015-05-28 15:52:44 -07001409 // The style of the label text is provided via the base TextView class. This is more
1410 // relevant than the style of the (optional) on/off text on the switch button itself,
1411 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001412 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001413 }
1414
Alan Viverettea54956a2015-01-07 16:05:02 -08001415 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001416 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001417 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1418 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001419 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1420 if (!TextUtils.isEmpty(switchText)) {
1421 CharSequence oldText = info.getText();
1422 if (TextUtils.isEmpty(oldText)) {
1423 info.setText(switchText);
1424 } else {
1425 StringBuilder newText = new StringBuilder();
1426 newText.append(oldText).append(' ').append(switchText);
1427 info.setText(newText);
1428 }
1429 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001430 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001431
1432 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1433 @Override
1434 public Float get(Switch object) {
1435 return object.mThumbPosition;
1436 }
1437
1438 @Override
1439 public void setValue(Switch object, float value) {
1440 object.setThumbPosition(value);
1441 }
1442 };
Adam Powell12190b32010-11-28 19:07:53 -08001443}