blob: ce1834e138ec263b25e6641b2a2dcb3b49cd6093 [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;
Adam Powell12190b32010-11-28 19:07:53 -080029import 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;
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -080035import android.os.Bundle;
Adam Powell12190b32010-11-28 19:07:53 -080036import android.text.Layout;
37import android.text.StaticLayout;
38import android.text.TextPaint;
39import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040040import android.text.method.AllCapsTransformationMethod;
41import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080042import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070043import android.util.FloatProperty;
44import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080045import android.view.Gravity;
46import android.view.MotionEvent;
Alan Viveretted4e77902014-10-27 17:50:51 -070047import android.view.SoundEffectConstants;
Adam Powell12190b32010-11-28 19:07:53 -080048import android.view.VelocityTracker;
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -080049import android.view.ViewAssistData;
Adam Powell12190b32010-11-28 19:07:53 -080050import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070051import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080052import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080053
Adam Powellbe0a4532010-11-29 17:47:48 -080054import com.android.internal.R;
55
Adam Powell12190b32010-11-28 19:07:53 -080056/**
57 * A Switch is a two-state toggle switch widget that can select between two
58 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070059 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
60 * property controls the text displayed in the label for the switch, whereas the
61 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
62 * controls the text on the thumb. Similarly, the
63 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
64 * setTypeface() methods control the typeface and style of label text, whereas the
65 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
66 * the related seSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080067 *
Scott Main4c359b72012-07-24 15:51:27 -070068 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
69 * guide.</p>
70 *
71 * @attr ref android.R.styleable#Switch_textOn
72 * @attr ref android.R.styleable#Switch_textOff
73 * @attr ref android.R.styleable#Switch_switchMinWidth
74 * @attr ref android.R.styleable#Switch_switchPadding
75 * @attr ref android.R.styleable#Switch_switchTextAppearance
76 * @attr ref android.R.styleable#Switch_thumb
77 * @attr ref android.R.styleable#Switch_thumbTextPadding
78 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080079 */
80public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070081 private static final int THUMB_ANIMATION_DURATION = 250;
82
Adam Powell12190b32010-11-28 19:07:53 -080083 private static final int TOUCH_MODE_IDLE = 0;
84 private static final int TOUCH_MODE_DOWN = 1;
85 private static final int TOUCH_MODE_DRAGGING = 2;
86
87 // Enum for the "typeface" XML parameter.
88 private static final int SANS = 1;
89 private static final int SERIF = 2;
90 private static final int MONOSPACE = 3;
91
92 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080093 private ColorStateList mThumbTintList = null;
94 private PorterDuff.Mode mThumbTintMode = null;
95 private boolean mHasThumbTint = false;
96 private boolean mHasThumbTintMode = false;
97
Adam Powell12190b32010-11-28 19:07:53 -080098 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080099 private ColorStateList mTrackTintList = null;
100 private PorterDuff.Mode mTrackTintMode = null;
101 private boolean mHasTrackTint = false;
102 private boolean mHasTrackTintMode = false;
103
Adam Powell12190b32010-11-28 19:07:53 -0800104 private int mThumbTextPadding;
105 private int mSwitchMinWidth;
106 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700107 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800108 private CharSequence mTextOn;
109 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700110 private boolean mShowText;
Adam Powell12190b32010-11-28 19:07:53 -0800111
112 private int mTouchMode;
113 private int mTouchSlop;
114 private float mTouchX;
115 private float mTouchY;
116 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
117 private int mMinFlingVelocity;
118
119 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700120
Alan Viverette0c0dde72014-07-30 13:29:39 -0700121 /**
122 * Width required to draw the switch track and thumb. Includes padding and
123 * optical bounds for both the track and thumb.
124 */
125 private int mSwitchWidth;
126
127 /**
128 * Height required to draw the switch track and thumb. Includes padding and
129 * optical bounds for both the track and thumb.
130 */
131 private int mSwitchHeight;
132
133 /**
134 * Width of the thumb's content region. Does not include padding or
135 * optical bounds.
136 */
137 private int mThumbWidth;
138
139 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800140 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700141
142 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800143 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700144
145 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800146 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700147
148 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800149 private int mSwitchBottom;
150
151 private TextPaint mTextPaint;
152 private ColorStateList mTextColors;
153 private Layout mOnLayout;
154 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400155 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700156 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800157
Adam Powellbe0a4532010-11-29 17:47:48 -0800158 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800159 private final Rect mTempRect = new Rect();
160
161 private static final int[] CHECKED_STATE_SET = {
162 R.attr.state_checked
163 };
164
165 /**
166 * Construct a new Switch with default styling.
167 *
168 * @param context The Context that will determine this widget's theming.
169 */
170 public Switch(Context context) {
171 this(context, null);
172 }
173
174 /**
175 * Construct a new Switch with default styling, overriding specific style
176 * attributes as requested.
177 *
178 * @param context The Context that will determine this widget's theming.
179 * @param attrs Specification of attributes that should deviate from default styling.
180 */
181 public Switch(Context context, AttributeSet attrs) {
182 this(context, attrs, com.android.internal.R.attr.switchStyle);
183 }
184
185 /**
186 * Construct a new Switch with a default style determined by the given theme attribute,
187 * overriding specific style attributes as requested.
188 *
189 * @param context The Context that will determine this widget's theming.
190 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700191 * @param defStyleAttr An attribute in the current theme that contains a
192 * reference to a style resource that supplies default values for
193 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800194 */
Alan Viverette617feb92013-09-09 18:09:13 -0700195 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
196 this(context, attrs, defStyleAttr, 0);
197 }
198
199
200 /**
201 * Construct a new Switch with a default style determined by the given theme
202 * attribute or style resource, overriding specific style attributes as
203 * requested.
204 *
205 * @param context The Context that will determine this widget's theming.
206 * @param attrs Specification of attributes that should deviate from the
207 * default styling.
208 * @param defStyleAttr An attribute in the current theme that contains a
209 * reference to a style resource that supplies default values for
210 * the view. Can be 0 to not look for defaults.
211 * @param defStyleRes A resource identifier of a style resource that
212 * supplies default values for the view, used only if
213 * defStyleAttr is 0 or can not be found in the theme. Can be 0
214 * to not look for defaults.
215 */
216 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
217 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800218
219 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700220
221 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800222 mTextPaint.density = res.getDisplayMetrics().density;
223 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
224
Alan Viverette617feb92013-09-09 18:09:13 -0700225 final TypedArray a = context.obtainStyledAttributes(
226 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700227 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700228 if (mThumbDrawable != null) {
229 mThumbDrawable.setCallback(this);
230 }
Chet Haase150176d2011-08-26 09:54:06 -0700231 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700232 if (mTrackDrawable != null) {
233 mTrackDrawable.setCallback(this);
234 }
Adam Powell12190b32010-11-28 19:07:53 -0800235 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
236 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700237 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800238 mThumbTextPadding = a.getDimensionPixelSize(
239 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
240 mSwitchMinWidth = a.getDimensionPixelSize(
241 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
242 mSwitchPadding = a.getDimensionPixelSize(
243 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700244 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800245
Alan Viverette661e6362014-05-12 10:55:37 -0700246 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800247 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
248 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700249 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800250 }
251 a.recycle();
252
Alan Viverette661e6362014-05-12 10:55:37 -0700253 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800254 mTouchSlop = config.getScaledTouchSlop();
255 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
256
257 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700258 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800259 setChecked(isChecked());
260 }
261
262 /**
263 * Sets the switch text color, size, style, hint color, and highlight color
264 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800265 *
266 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800267 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700268 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800269 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700270 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800271 com.android.internal.R.styleable.TextAppearance);
272
273 ColorStateList colors;
274 int ts;
275
276 colors = appearance.getColorStateList(com.android.internal.R.styleable.
277 TextAppearance_textColor);
278 if (colors != null) {
279 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700280 } else {
281 // If no color set in TextAppearance, default to the view's textColor
282 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800283 }
284
285 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
286 TextAppearance_textSize, 0);
287 if (ts != 0) {
288 if (ts != mTextPaint.getTextSize()) {
289 mTextPaint.setTextSize(ts);
290 requestLayout();
291 }
292 }
293
294 int typefaceIndex, styleIndex;
295
296 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
297 TextAppearance_typeface, -1);
298 styleIndex = appearance.getInt(com.android.internal.R.styleable.
299 TextAppearance_textStyle, -1);
300
301 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
302
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400303 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
304 TextAppearance_textAllCaps, false);
305 if (allCaps) {
306 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
307 mSwitchTransformationMethod.setLengthChangesAllowed(true);
308 } else {
309 mSwitchTransformationMethod = null;
310 }
311
Adam Powell12190b32010-11-28 19:07:53 -0800312 appearance.recycle();
313 }
314
315 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
316 Typeface tf = null;
317 switch (typefaceIndex) {
318 case SANS:
319 tf = Typeface.SANS_SERIF;
320 break;
321
322 case SERIF:
323 tf = Typeface.SERIF;
324 break;
325
326 case MONOSPACE:
327 tf = Typeface.MONOSPACE;
328 break;
329 }
330
331 setSwitchTypeface(tf, styleIndex);
332 }
333
334 /**
335 * Sets the typeface and style in which the text should be displayed on the
336 * switch, and turns on the fake bold and italic bits in the Paint if the
337 * Typeface that you provided does not have all the bits in the
338 * style that you specified.
339 */
340 public void setSwitchTypeface(Typeface tf, int style) {
341 if (style > 0) {
342 if (tf == null) {
343 tf = Typeface.defaultFromStyle(style);
344 } else {
345 tf = Typeface.create(tf, style);
346 }
347
348 setSwitchTypeface(tf);
349 // now compute what (if any) algorithmic styling is needed
350 int typefaceStyle = tf != null ? tf.getStyle() : 0;
351 int need = style & ~typefaceStyle;
352 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
353 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
354 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700355 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800356 mTextPaint.setTextSkewX(0);
357 setSwitchTypeface(tf);
358 }
359 }
360
361 /**
Chet Haase150176d2011-08-26 09:54:06 -0700362 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800363 * Note that not all Typeface families actually have bold and italic
364 * variants, so you may need to use
365 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
366 * that you actually want.
367 *
368 * @attr ref android.R.styleable#TextView_typeface
369 * @attr ref android.R.styleable#TextView_textStyle
370 */
371 public void setSwitchTypeface(Typeface tf) {
372 if (mTextPaint.getTypeface() != tf) {
373 mTextPaint.setTypeface(tf);
374
375 requestLayout();
376 invalidate();
377 }
378 }
379
380 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800381 * Set the amount of horizontal padding between the switch and the associated text.
382 *
383 * @param pixels Amount of padding in pixels
384 *
385 * @attr ref android.R.styleable#Switch_switchPadding
386 */
387 public void setSwitchPadding(int pixels) {
388 mSwitchPadding = pixels;
389 requestLayout();
390 }
391
392 /**
393 * Get the amount of horizontal padding between the switch and the associated text.
394 *
395 * @return Amount of padding in pixels
396 *
397 * @attr ref android.R.styleable#Switch_switchPadding
398 */
399 public int getSwitchPadding() {
400 return mSwitchPadding;
401 }
402
403 /**
404 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
405 * of this value and its measured width as determined by the switch drawables and text used.
406 *
407 * @param pixels Minimum width of the switch in pixels
408 *
409 * @attr ref android.R.styleable#Switch_switchMinWidth
410 */
411 public void setSwitchMinWidth(int pixels) {
412 mSwitchMinWidth = pixels;
413 requestLayout();
414 }
415
416 /**
417 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
418 * of this value and its measured width as determined by the switch drawables and text used.
419 *
420 * @return Minimum width of the switch in pixels
421 *
422 * @attr ref android.R.styleable#Switch_switchMinWidth
423 */
424 public int getSwitchMinWidth() {
425 return mSwitchMinWidth;
426 }
427
428 /**
429 * Set the horizontal padding around the text drawn on the switch itself.
430 *
431 * @param pixels Horizontal padding for switch thumb text in pixels
432 *
433 * @attr ref android.R.styleable#Switch_thumbTextPadding
434 */
435 public void setThumbTextPadding(int pixels) {
436 mThumbTextPadding = pixels;
437 requestLayout();
438 }
439
440 /**
441 * Get the horizontal padding around the text drawn on the switch itself.
442 *
443 * @return Horizontal padding for switch thumb text in pixels
444 *
445 * @attr ref android.R.styleable#Switch_thumbTextPadding
446 */
447 public int getThumbTextPadding() {
448 return mThumbTextPadding;
449 }
450
451 /**
452 * Set the drawable used for the track that the switch slides within.
453 *
454 * @param track Track drawable
455 *
456 * @attr ref android.R.styleable#Switch_track
457 */
458 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700459 if (mTrackDrawable != null) {
460 mTrackDrawable.setCallback(null);
461 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800462 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700463 if (track != null) {
464 track.setCallback(this);
465 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800466 requestLayout();
467 }
468
469 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800470 * Set the drawable used for the track that the switch slides within.
471 *
Adam Powelldca510e2012-03-08 20:06:39 -0800472 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800473 *
474 * @attr ref android.R.styleable#Switch_track
475 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700476 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800477 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800478 }
479
480 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800481 * Get the drawable used for the track that the switch slides within.
482 *
483 * @return Track drawable
484 *
485 * @attr ref android.R.styleable#Switch_track
486 */
487 public Drawable getTrackDrawable() {
488 return mTrackDrawable;
489 }
490
491 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800492 * Applies a tint to the track drawable. Does not modify the current
493 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
494 * <p>
495 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
496 * automatically mutate the drawable and apply the specified tint and tint
497 * mode using {@link Drawable#setTintList(ColorStateList)}.
498 *
499 * @param tint the tint to apply, may be {@code null} to clear tint
500 *
501 * @attr ref android.R.styleable#Switch_trackTint
502 * @see #getTrackTintList()
503 * @see Drawable#setTintList(ColorStateList)
504 */
505 public void setTrackTintList(@Nullable ColorStateList tint) {
506 mTrackTintList = tint;
507 mHasTrackTint = true;
508
509 applyTrackTint();
510 }
511
512 /**
513 * @return the tint applied to the track drawable
514 * @attr ref android.R.styleable#Switch_trackTint
515 * @see #setTrackTintList(ColorStateList)
516 */
517 @Nullable
518 public ColorStateList getTrackTintList() {
519 return mTrackTintList;
520 }
521
522 /**
523 * Specifies the blending mode used to apply the tint specified by
524 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
525 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
526 *
527 * @param tintMode the blending mode used to apply the tint, may be
528 * {@code null} to clear tint
529 * @attr ref android.R.styleable#Switch_trackTintMode
530 * @see #getTrackTintMode()
531 * @see Drawable#setTintMode(PorterDuff.Mode)
532 */
533 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
534 mTrackTintMode = tintMode;
535 mHasTrackTintMode = true;
536
537 applyTrackTint();
538 }
539
540 /**
541 * @return the blending mode used to apply the tint to the track
542 * drawable
543 * @attr ref android.R.styleable#Switch_trackTintMode
544 * @see #setTrackTintMode(PorterDuff.Mode)
545 */
546 @Nullable
547 public PorterDuff.Mode getTrackTintMode() {
548 return mTrackTintMode;
549 }
550
551 private void applyTrackTint() {
552 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
553 mTrackDrawable = mTrackDrawable.mutate();
554
555 if (mHasTrackTint) {
556 mTrackDrawable.setTintList(mTrackTintList);
557 }
558
559 if (mHasTrackTintMode) {
560 mTrackDrawable.setTintMode(mTrackTintMode);
561 }
562
563 // The drawable (or one of its children) may not have been
564 // stateful before applying the tint, so let's try again.
565 if (mTrackDrawable.isStateful()) {
566 mTrackDrawable.setState(getDrawableState());
567 }
568 }
569 }
570
571 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800572 * Set the drawable used for the switch "thumb" - the piece that the user
573 * can physically touch and drag along the track.
574 *
575 * @param thumb Thumb drawable
576 *
577 * @attr ref android.R.styleable#Switch_thumb
578 */
579 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700580 if (mThumbDrawable != null) {
581 mThumbDrawable.setCallback(null);
582 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800583 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700584 if (thumb != null) {
585 thumb.setCallback(this);
586 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800587 requestLayout();
588 }
589
590 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800591 * Set the drawable used for the switch "thumb" - the piece that the user
592 * can physically touch and drag along the track.
593 *
Adam Powelldca510e2012-03-08 20:06:39 -0800594 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800595 *
596 * @attr ref android.R.styleable#Switch_thumb
597 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700598 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800599 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800600 }
601
602 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800603 * Get the drawable used for the switch "thumb" - the piece that the user
604 * can physically touch and drag along the track.
605 *
606 * @return Thumb drawable
607 *
608 * @attr ref android.R.styleable#Switch_thumb
609 */
610 public Drawable getThumbDrawable() {
611 return mThumbDrawable;
612 }
613
614 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800615 * Applies a tint to the thumb drawable. Does not modify the current
616 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
617 * <p>
618 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
619 * automatically mutate the drawable and apply the specified tint and tint
620 * mode using {@link Drawable#setTintList(ColorStateList)}.
621 *
622 * @param tint the tint to apply, may be {@code null} to clear tint
623 *
624 * @attr ref android.R.styleable#Switch_thumbTint
625 * @see #getThumbTintList()
626 * @see Drawable#setTintList(ColorStateList)
627 */
628 public void setThumbTintList(@Nullable ColorStateList tint) {
629 mThumbTintList = tint;
630 mHasThumbTint = true;
631
632 applyThumbTint();
633 }
634
635 /**
636 * @return the tint applied to the thumb drawable
637 * @attr ref android.R.styleable#Switch_thumbTint
638 * @see #setThumbTintList(ColorStateList)
639 */
640 @Nullable
641 public ColorStateList getThumbTintList() {
642 return mThumbTintList;
643 }
644
645 /**
646 * Specifies the blending mode used to apply the tint specified by
647 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
648 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
649 *
650 * @param tintMode the blending mode used to apply the tint, may be
651 * {@code null} to clear tint
652 * @attr ref android.R.styleable#Switch_thumbTintMode
653 * @see #getThumbTintMode()
654 * @see Drawable#setTintMode(PorterDuff.Mode)
655 */
656 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
657 mThumbTintMode = tintMode;
658 mHasThumbTintMode = true;
659
660 applyThumbTint();
661 }
662
663 /**
664 * @return the blending mode used to apply the tint to the thumb
665 * drawable
666 * @attr ref android.R.styleable#Switch_thumbTintMode
667 * @see #setThumbTintMode(PorterDuff.Mode)
668 */
669 @Nullable
670 public PorterDuff.Mode getThumbTintMode() {
671 return mThumbTintMode;
672 }
673
674 private void applyThumbTint() {
675 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
676 mThumbDrawable = mThumbDrawable.mutate();
677
678 if (mHasThumbTint) {
679 mThumbDrawable.setTintList(mThumbTintList);
680 }
681
682 if (mHasThumbTintMode) {
683 mThumbDrawable.setTintMode(mThumbTintMode);
684 }
685
686 // The drawable (or one of its children) may not have been
687 // stateful before applying the tint, so let's try again.
688 if (mThumbDrawable.isStateful()) {
689 mThumbDrawable.setState(getDrawableState());
690 }
691 }
692 }
693
694 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700695 * Specifies whether the track should be split by the thumb. When true,
696 * the thumb's optical bounds will be clipped out of the track drawable,
697 * then the thumb will be drawn into the resulting gap.
698 *
699 * @param splitTrack Whether the track should be split by the thumb
700 *
701 * @attr ref android.R.styleable#Switch_splitTrack
702 */
703 public void setSplitTrack(boolean splitTrack) {
704 mSplitTrack = splitTrack;
705 invalidate();
706 }
707
708 /**
709 * Returns whether the track should be split by the thumb.
710 *
711 * @attr ref android.R.styleable#Switch_splitTrack
712 */
713 public boolean getSplitTrack() {
714 return mSplitTrack;
715 }
716
717 /**
Chet Haase150176d2011-08-26 09:54:06 -0700718 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800719 *
720 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800721 */
722 public CharSequence getTextOn() {
723 return mTextOn;
724 }
725
726 /**
Chet Haase150176d2011-08-26 09:54:06 -0700727 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800728 *
729 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800730 */
731 public void setTextOn(CharSequence textOn) {
732 mTextOn = textOn;
733 requestLayout();
734 }
735
736 /**
Chet Haase150176d2011-08-26 09:54:06 -0700737 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800738 *
739 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800740 */
741 public CharSequence getTextOff() {
742 return mTextOff;
743 }
744
745 /**
Chet Haase150176d2011-08-26 09:54:06 -0700746 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800747 *
748 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800749 */
750 public void setTextOff(CharSequence textOff) {
751 mTextOff = textOff;
752 requestLayout();
753 }
754
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700755 /**
756 * Sets whether the on/off text should be displayed.
757 *
758 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700759 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700760 */
761 public void setShowText(boolean showText) {
762 if (mShowText != showText) {
763 mShowText = showText;
764 requestLayout();
765 }
766 }
767
768 /**
769 * @return whether the on/off text should be displayed
Alan Viverette0c0dde72014-07-30 13:29:39 -0700770 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700771 */
772 public boolean getShowText() {
773 return mShowText;
774 }
775
Adam Powell12190b32010-11-28 19:07:53 -0800776 @Override
777 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700778 if (mShowText) {
779 if (mOnLayout == null) {
780 mOnLayout = makeLayout(mTextOn);
781 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800782
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700783 if (mOffLayout == null) {
784 mOffLayout = makeLayout(mTextOff);
785 }
Adam Powell12190b32010-11-28 19:07:53 -0800786 }
787
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000788 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700789 final int thumbWidth;
790 final int thumbHeight;
791 if (mThumbDrawable != null) {
792 // Cached thumb width does not include padding.
793 mThumbDrawable.getPadding(padding);
794 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
795 thumbHeight = mThumbDrawable.getIntrinsicHeight();
796 } else {
797 thumbWidth = 0;
798 thumbHeight = 0;
799 }
800
801 final int maxTextWidth;
802 if (mShowText) {
803 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
804 + mThumbTextPadding * 2;
805 } else {
806 maxTextWidth = 0;
807 }
808
809 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
810
811 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700812 if (mTrackDrawable != null) {
813 mTrackDrawable.getPadding(padding);
814 trackHeight = mTrackDrawable.getIntrinsicHeight();
815 } else {
816 padding.setEmpty();
817 trackHeight = 0;
818 }
819
Alan Viverette0c0dde72014-07-30 13:29:39 -0700820 // Adjust left and right padding to ensure there's enough room for the
821 // thumb's padding (when present).
822 int paddingLeft = padding.left;
823 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700824 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700825 final Insets inset = mThumbDrawable.getOpticalInsets();
826 paddingLeft = Math.max(paddingLeft, inset.left);
827 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700828 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800829
Adam Powell12190b32010-11-28 19:07:53 -0800830 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700831 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700832 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800833 mSwitchWidth = switchWidth;
834 mSwitchHeight = switchHeight;
835
836 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700837
Adam Powell12190b32010-11-28 19:07:53 -0800838 final int measuredHeight = getMeasuredHeight();
839 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800840 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800841 }
842 }
843
Alan Viverettea54956a2015-01-07 16:05:02 -0800844 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700845 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800846 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
847 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700848
849 final CharSequence text = isChecked() ? mTextOn : mTextOff;
850 if (text != null) {
851 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700852 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700853 }
854
Adam Powell12190b32010-11-28 19:07:53 -0800855 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400856 final CharSequence transformed = (mSwitchTransformationMethod != null)
857 ? mSwitchTransformationMethod.getTransformation(text, this)
858 : text;
859
860 return new StaticLayout(transformed, mTextPaint,
861 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800862 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
863 }
864
865 /**
866 * @return true if (x, y) is within the target area of the switch thumb
867 */
868 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800869 if (mThumbDrawable == null) {
870 return false;
871 }
872
Alan Viverettecc2688d2013-09-17 17:00:12 -0700873 // Relies on mTempRect, MUST be called first!
874 final int thumbOffset = getThumbOffset();
875
Adam Powell12190b32010-11-28 19:07:53 -0800876 mThumbDrawable.getPadding(mTempRect);
877 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700878 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800879 final int thumbRight = thumbLeft + mThumbWidth +
880 mTempRect.left + mTempRect.right + mTouchSlop;
881 final int thumbBottom = mSwitchBottom + mTouchSlop;
882 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
883 }
884
885 @Override
886 public boolean onTouchEvent(MotionEvent ev) {
887 mVelocityTracker.addMovement(ev);
888 final int action = ev.getActionMasked();
889 switch (action) {
890 case MotionEvent.ACTION_DOWN: {
891 final float x = ev.getX();
892 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700893 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800894 mTouchMode = TOUCH_MODE_DOWN;
895 mTouchX = x;
896 mTouchY = y;
897 }
898 break;
899 }
900
901 case MotionEvent.ACTION_MOVE: {
902 switch (mTouchMode) {
903 case TOUCH_MODE_IDLE:
904 // Didn't target the thumb, treat normally.
905 break;
906
907 case TOUCH_MODE_DOWN: {
908 final float x = ev.getX();
909 final float y = ev.getY();
910 if (Math.abs(x - mTouchX) > mTouchSlop ||
911 Math.abs(y - mTouchY) > mTouchSlop) {
912 mTouchMode = TOUCH_MODE_DRAGGING;
913 getParent().requestDisallowInterceptTouchEvent(true);
914 mTouchX = x;
915 mTouchY = y;
916 return true;
917 }
918 break;
919 }
920
921 case TOUCH_MODE_DRAGGING: {
922 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700923 final int thumbScrollRange = getThumbScrollRange();
924 final float thumbScrollOffset = x - mTouchX;
925 float dPos;
926 if (thumbScrollRange != 0) {
927 dPos = thumbScrollOffset / thumbScrollRange;
928 } else {
929 // If the thumb scroll range is empty, just use the
930 // movement direction to snap on or off.
931 dPos = thumbScrollOffset > 0 ? 1 : -1;
932 }
933 if (isLayoutRtl()) {
934 dPos = -dPos;
935 }
936 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800937 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800938 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700939 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800940 }
941 return true;
942 }
943 }
944 break;
945 }
946
947 case MotionEvent.ACTION_UP:
948 case MotionEvent.ACTION_CANCEL: {
949 if (mTouchMode == TOUCH_MODE_DRAGGING) {
950 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700951 // Allow super class to handle pressed state, etc.
952 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800953 return true;
954 }
955 mTouchMode = TOUCH_MODE_IDLE;
956 mVelocityTracker.clear();
957 break;
958 }
959 }
960
961 return super.onTouchEvent(ev);
962 }
963
964 private void cancelSuperTouch(MotionEvent ev) {
965 MotionEvent cancel = MotionEvent.obtain(ev);
966 cancel.setAction(MotionEvent.ACTION_CANCEL);
967 super.onTouchEvent(cancel);
968 cancel.recycle();
969 }
970
971 /**
972 * Called from onTouchEvent to end a drag operation.
973 *
974 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
975 */
976 private void stopDrag(MotionEvent ev) {
977 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -0800978
Alan Viverette86453ff2013-09-26 14:46:08 -0700979 // Commit the change if the event is up and not canceled and the switch
980 // has not been disabled during the drag.
981 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -0700982 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -0700983 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -0800984 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -0800985 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -0700986 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -0800987 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700988 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -0800989 } else {
990 newState = getTargetCheckedState();
991 }
Adam Powell12190b32010-11-28 19:07:53 -0800992 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -0700993 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -0800994 }
Alan Viverette86453ff2013-09-26 14:46:08 -0700995
Alan Viveretted4e77902014-10-27 17:50:51 -0700996 if (newState != oldState) {
997 playSoundEffect(SoundEffectConstants.CLICK);
998 setChecked(newState);
999 }
1000
Alan Viverette86453ff2013-09-26 14:46:08 -07001001 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001002 }
1003
1004 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001005 final float targetPosition = newCheckedState ? 1 : 0;
1006 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1007 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1008 mPositionAnimator.setAutoCancel(true);
1009 mPositionAnimator.start();
1010 }
1011
1012 private void cancelPositionAnimator() {
1013 if (mPositionAnimator != null) {
1014 mPositionAnimator.cancel();
1015 }
Adam Powell12190b32010-11-28 19:07:53 -08001016 }
1017
1018 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001019 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001020 }
1021
Alan Viverettecc2688d2013-09-17 17:00:12 -07001022 /**
1023 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1024 *
1025 * @param position new position between [0,1]
1026 */
1027 private void setThumbPosition(float position) {
1028 mThumbPosition = position;
1029 invalidate();
1030 }
1031
1032 @Override
1033 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001034 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001035 }
1036
1037 @Override
1038 public void setChecked(boolean checked) {
1039 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001040
Alan Viverette467d6292014-08-12 15:13:19 -07001041 // Calling the super method may result in setChecked() getting called
1042 // recursively with a different value, so load the REAL value...
1043 checked = isChecked();
1044
Alan Viverette86453ff2013-09-26 14:46:08 -07001045 if (isAttachedToWindow() && isLaidOut()) {
1046 animateThumbToCheckedState(checked);
1047 } else {
1048 // Immediately move the thumb to the new position.
1049 cancelPositionAnimator();
1050 setThumbPosition(checked ? 1 : 0);
1051 }
Adam Powell12190b32010-11-28 19:07:53 -08001052 }
1053
1054 @Override
1055 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1056 super.onLayout(changed, left, top, right, bottom);
1057
Alan Viverette0c0dde72014-07-30 13:29:39 -07001058 int opticalInsetLeft = 0;
1059 int opticalInsetRight = 0;
1060 if (mThumbDrawable != null) {
1061 final Rect trackPadding = mTempRect;
1062 if (mTrackDrawable != null) {
1063 mTrackDrawable.getPadding(trackPadding);
1064 } else {
1065 trackPadding.setEmpty();
1066 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001067
Alan Viverette0c0dde72014-07-30 13:29:39 -07001068 final Insets insets = mThumbDrawable.getOpticalInsets();
1069 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1070 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001071 }
1072
Alan Viverette0c0dde72014-07-30 13:29:39 -07001073 final int switchRight;
1074 final int switchLeft;
1075 if (isLayoutRtl()) {
1076 switchLeft = getPaddingLeft() + opticalInsetLeft;
1077 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1078 } else {
1079 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1080 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1081 }
1082
1083 final int switchTop;
1084 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001085 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1086 default:
1087 case Gravity.TOP:
1088 switchTop = getPaddingTop();
1089 switchBottom = switchTop + mSwitchHeight;
1090 break;
1091
1092 case Gravity.CENTER_VERTICAL:
1093 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1094 mSwitchHeight / 2;
1095 switchBottom = switchTop + mSwitchHeight;
1096 break;
1097
1098 case Gravity.BOTTOM:
1099 switchBottom = getHeight() - getPaddingBottom();
1100 switchTop = switchBottom - mSwitchHeight;
1101 break;
1102 }
1103
1104 mSwitchLeft = switchLeft;
1105 mSwitchTop = switchTop;
1106 mSwitchBottom = switchBottom;
1107 mSwitchRight = switchRight;
1108 }
1109
1110 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001111 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001112 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001113 final int switchLeft = mSwitchLeft;
1114 final int switchTop = mSwitchTop;
1115 final int switchRight = mSwitchRight;
1116 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001117
1118 int thumbInitialLeft = switchLeft + getThumbOffset();
1119
1120 final Insets thumbInsets;
1121 if (mThumbDrawable != null) {
1122 thumbInsets = mThumbDrawable.getOpticalInsets();
1123 } else {
1124 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001125 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001126
Alan Viverette0c0dde72014-07-30 13:29:39 -07001127 // Layout the track.
1128 if (mTrackDrawable != null) {
1129 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001130
Alan Viverette0c0dde72014-07-30 13:29:39 -07001131 // Adjust thumb position for track padding.
1132 thumbInitialLeft += padding.left;
1133
1134 // If necessary, offset by the optical insets of the thumb asset.
1135 int trackLeft = switchLeft;
1136 int trackTop = switchTop;
1137 int trackRight = switchRight;
1138 int trackBottom = switchBottom;
1139 if (thumbInsets != Insets.NONE) {
1140 if (thumbInsets.left > padding.left) {
1141 trackLeft += thumbInsets.left - padding.left;
1142 }
1143 if (thumbInsets.top > padding.top) {
1144 trackTop += thumbInsets.top - padding.top;
1145 }
1146 if (thumbInsets.right > padding.right) {
1147 trackRight -= thumbInsets.right - padding.right;
1148 }
1149 if (thumbInsets.bottom > padding.bottom) {
1150 trackBottom -= thumbInsets.bottom - padding.bottom;
1151 }
1152 }
1153 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1154 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001155
Alan Viverette661e6362014-05-12 10:55:37 -07001156 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001157 if (mThumbDrawable != null) {
1158 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001159
1160 final int thumbLeft = thumbInitialLeft - padding.left;
1161 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001162 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001163
Alan Viverette4d065a02014-07-11 15:28:38 -07001164 final Drawable background = getBackground();
1165 if (background != null) {
1166 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1167 }
Alan Viverette61956602014-04-22 19:07:06 -07001168 }
1169
Alan Viverettead2f8e32014-05-16 13:28:33 -07001170 // Draw the background.
1171 super.draw(c);
1172 }
1173
1174 @Override
1175 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001176 super.onDraw(canvas);
1177
Alan Viverette4d065a02014-07-11 15:28:38 -07001178 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001179 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001180 if (trackDrawable != null) {
1181 trackDrawable.getPadding(padding);
1182 } else {
1183 padding.setEmpty();
1184 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001185
1186 final int switchTop = mSwitchTop;
1187 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001188 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001189 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001190
Alan Viverette4d065a02014-07-11 15:28:38 -07001191 final Drawable thumbDrawable = mThumbDrawable;
1192 if (trackDrawable != null) {
1193 if (mSplitTrack && thumbDrawable != null) {
1194 final Insets insets = thumbDrawable.getOpticalInsets();
1195 thumbDrawable.copyBounds(padding);
1196 padding.left += insets.left;
1197 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001198
Alan Viverette4d065a02014-07-11 15:28:38 -07001199 final int saveCount = canvas.save();
1200 canvas.clipRect(padding, Op.DIFFERENCE);
1201 trackDrawable.draw(canvas);
1202 canvas.restoreToCount(saveCount);
1203 } else {
1204 trackDrawable.draw(canvas);
1205 }
Alan Viverette661e6362014-05-12 10:55:37 -07001206 }
Alan Viverette61956602014-04-22 19:07:06 -07001207
1208 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001209
1210 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001211 thumbDrawable.draw(canvas);
1212 }
Adam Powell12190b32010-11-28 19:07:53 -08001213
Alan Viverette5876ff42014-03-03 17:40:46 -08001214 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001215 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001216 final int drawableState[] = getDrawableState();
1217 if (mTextColors != null) {
1218 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1219 }
1220 mTextPaint.drawableState = drawableState;
1221
Alan Viverette4d065a02014-07-11 15:28:38 -07001222 final int cX;
1223 if (thumbDrawable != null) {
1224 final Rect bounds = thumbDrawable.getBounds();
1225 cX = bounds.left + bounds.right;
1226 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001227 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001228 }
1229
1230 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001231 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1232 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001233 switchText.draw(canvas);
1234 }
Adam Powell12190b32010-11-28 19:07:53 -08001235
Alan Viverette5876ff42014-03-03 17:40:46 -08001236 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001237 }
1238
1239 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001240 public int getCompoundPaddingLeft() {
1241 if (!isLayoutRtl()) {
1242 return super.getCompoundPaddingLeft();
1243 }
1244 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1245 if (!TextUtils.isEmpty(getText())) {
1246 padding += mSwitchPadding;
1247 }
1248 return padding;
1249 }
1250
1251 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001252 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001253 if (isLayoutRtl()) {
1254 return super.getCompoundPaddingRight();
1255 }
Adam Powell12190b32010-11-28 19:07:53 -08001256 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1257 if (!TextUtils.isEmpty(getText())) {
1258 padding += mSwitchPadding;
1259 }
1260 return padding;
1261 }
1262
Alan Viverettecc2688d2013-09-17 17:00:12 -07001263 /**
1264 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001265 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001266 *
1267 * @return thumb offset
1268 */
1269 private int getThumbOffset() {
1270 final float thumbPosition;
1271 if (isLayoutRtl()) {
1272 thumbPosition = 1 - mThumbPosition;
1273 } else {
1274 thumbPosition = mThumbPosition;
1275 }
1276 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1277 }
1278
Adam Powell12190b32010-11-28 19:07:53 -08001279 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001280 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001281 final Rect padding = mTempRect;
1282 mTrackDrawable.getPadding(padding);
1283
1284 final Insets insets;
1285 if (mThumbDrawable != null) {
1286 insets = mThumbDrawable.getOpticalInsets();
1287 } else {
1288 insets = Insets.NONE;
1289 }
1290
1291 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1292 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001293 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001294 return 0;
1295 }
Adam Powell12190b32010-11-28 19:07:53 -08001296 }
1297
1298 @Override
1299 protected int[] onCreateDrawableState(int extraSpace) {
1300 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1301 if (isChecked()) {
1302 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1303 }
1304 return drawableState;
1305 }
1306
1307 @Override
1308 protected void drawableStateChanged() {
1309 super.drawableStateChanged();
1310
Alan Viverette661e6362014-05-12 10:55:37 -07001311 final int[] myDrawableState = getDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -08001312
Alan Viverette2356c5e2014-05-22 22:43:59 -07001313 if (mThumbDrawable != null) {
1314 mThumbDrawable.setState(myDrawableState);
Alan Viverette661e6362014-05-12 10:55:37 -07001315 }
1316
1317 if (mTrackDrawable != null) {
1318 mTrackDrawable.setState(myDrawableState);
1319 }
Adam Powell12190b32010-11-28 19:07:53 -08001320
1321 invalidate();
1322 }
1323
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001324 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001325 public void drawableHotspotChanged(float x, float y) {
1326 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001327
1328 if (mThumbDrawable != null) {
1329 mThumbDrawable.setHotspot(x, y);
1330 }
1331
1332 if (mTrackDrawable != null) {
1333 mTrackDrawable.setHotspot(x, y);
1334 }
1335 }
1336
Adam Powell12190b32010-11-28 19:07:53 -08001337 @Override
1338 protected boolean verifyDrawable(Drawable who) {
1339 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1340 }
1341
1342 @Override
1343 public void jumpDrawablesToCurrentState() {
1344 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001345
1346 if (mThumbDrawable != null) {
1347 mThumbDrawable.jumpToCurrentState();
1348 }
1349
1350 if (mTrackDrawable != null) {
1351 mTrackDrawable.jumpToCurrentState();
1352 }
1353
1354 if (mPositionAnimator != null && mPositionAnimator.isRunning()) {
1355 mPositionAnimator.end();
1356 mPositionAnimator = null;
1357 }
Adam Powell12190b32010-11-28 19:07:53 -08001358 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001359
1360 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001361 public CharSequence getAccessibilityClassName() {
1362 return Switch.class.getName();
1363 }
1364
1365 @Override
1366 public void onProvideAssistData(ViewAssistData data, Bundle extras) {
1367 super.onProvideAssistData(data, extras);
1368 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1369 if (!TextUtils.isEmpty(switchText)) {
1370 CharSequence oldText = data.getText();
1371 if (TextUtils.isEmpty(oldText)) {
1372 data.setText(switchText);
1373 } else {
1374 StringBuilder newText = new StringBuilder();
1375 newText.append(oldText).append(' ').append(switchText);
1376 data.setText(newText);
1377 }
1378 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001379 }
1380
Alan Viverettea54956a2015-01-07 16:05:02 -08001381 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001382 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001383 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1384 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001385 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1386 if (!TextUtils.isEmpty(switchText)) {
1387 CharSequence oldText = info.getText();
1388 if (TextUtils.isEmpty(oldText)) {
1389 info.setText(switchText);
1390 } else {
1391 StringBuilder newText = new StringBuilder();
1392 newText.append(oldText).append(' ').append(switchText);
1393 info.setText(newText);
1394 }
1395 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001396 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001397
1398 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1399 @Override
1400 public Float get(Switch object) {
1401 return object.mThumbPosition;
1402 }
1403
1404 @Override
1405 public void setValue(Switch object, float value) {
1406 object.setThumbPosition(value);
1407 }
1408 };
Adam Powell12190b32010-11-28 19:07:53 -08001409}