blob: 438e16476e0daeae50363a5a31b3cc7f99c3c9a7 [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;
Adam Powell12190b32010-11-28 19:07:53 -080020import android.content.Context;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
Alan Viverette661e6362014-05-12 10:55:37 -070025import android.graphics.Insets;
Adam Powell12190b32010-11-28 19:07:53 -080026import android.graphics.Paint;
27import android.graphics.Rect;
28import android.graphics.Typeface;
Alan Viverette661e6362014-05-12 10:55:37 -070029import android.graphics.Region.Op;
Adam Powell12190b32010-11-28 19:07:53 -080030import android.graphics.drawable.Drawable;
31import android.text.Layout;
32import android.text.StaticLayout;
33import android.text.TextPaint;
34import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040035import android.text.method.AllCapsTransformationMethod;
36import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080037import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070038import android.util.FloatProperty;
39import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080040import android.view.Gravity;
41import android.view.MotionEvent;
42import android.view.VelocityTracker;
43import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070044import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080045import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080046
Adam Powellbe0a4532010-11-29 17:47:48 -080047import com.android.internal.R;
48
Adam Powell12190b32010-11-28 19:07:53 -080049/**
50 * A Switch is a two-state toggle switch widget that can select between two
51 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070052 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
53 * property controls the text displayed in the label for the switch, whereas the
54 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
55 * controls the text on the thumb. Similarly, the
56 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
57 * setTypeface() methods control the typeface and style of label text, whereas the
58 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
59 * the related seSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080060 *
Scott Main4c359b72012-07-24 15:51:27 -070061 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
62 * guide.</p>
63 *
64 * @attr ref android.R.styleable#Switch_textOn
65 * @attr ref android.R.styleable#Switch_textOff
66 * @attr ref android.R.styleable#Switch_switchMinWidth
67 * @attr ref android.R.styleable#Switch_switchPadding
68 * @attr ref android.R.styleable#Switch_switchTextAppearance
69 * @attr ref android.R.styleable#Switch_thumb
70 * @attr ref android.R.styleable#Switch_thumbTextPadding
71 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080072 */
73public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070074 private static final int THUMB_ANIMATION_DURATION = 250;
75
Adam Powell12190b32010-11-28 19:07:53 -080076 private static final int TOUCH_MODE_IDLE = 0;
77 private static final int TOUCH_MODE_DOWN = 1;
78 private static final int TOUCH_MODE_DRAGGING = 2;
79
80 // Enum for the "typeface" XML parameter.
81 private static final int SANS = 1;
82 private static final int SERIF = 2;
83 private static final int MONOSPACE = 3;
84
85 private Drawable mThumbDrawable;
86 private Drawable mTrackDrawable;
87 private int mThumbTextPadding;
88 private int mSwitchMinWidth;
89 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -070090 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -080091 private CharSequence mTextOn;
92 private CharSequence mTextOff;
93
94 private int mTouchMode;
95 private int mTouchSlop;
96 private float mTouchX;
97 private float mTouchY;
98 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
99 private int mMinFlingVelocity;
100
101 private float mThumbPosition;
102 private int mSwitchWidth;
103 private int mSwitchHeight;
104 private int mThumbWidth; // Does not include padding
105
106 private int mSwitchLeft;
107 private int mSwitchTop;
108 private int mSwitchRight;
109 private int mSwitchBottom;
110
111 private TextPaint mTextPaint;
112 private ColorStateList mTextColors;
113 private Layout mOnLayout;
114 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400115 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700116 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800117
Adam Powellbe0a4532010-11-29 17:47:48 -0800118 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800119 private final Rect mTempRect = new Rect();
120
121 private static final int[] CHECKED_STATE_SET = {
122 R.attr.state_checked
123 };
124
125 /**
126 * Construct a new Switch with default styling.
127 *
128 * @param context The Context that will determine this widget's theming.
129 */
130 public Switch(Context context) {
131 this(context, null);
132 }
133
134 /**
135 * Construct a new Switch with default styling, overriding specific style
136 * attributes as requested.
137 *
138 * @param context The Context that will determine this widget's theming.
139 * @param attrs Specification of attributes that should deviate from default styling.
140 */
141 public Switch(Context context, AttributeSet attrs) {
142 this(context, attrs, com.android.internal.R.attr.switchStyle);
143 }
144
145 /**
146 * Construct a new Switch with a default style determined by the given theme attribute,
147 * overriding specific style attributes as requested.
148 *
149 * @param context The Context that will determine this widget's theming.
150 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700151 * @param defStyleAttr An attribute in the current theme that contains a
152 * reference to a style resource that supplies default values for
153 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800154 */
Alan Viverette617feb92013-09-09 18:09:13 -0700155 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
156 this(context, attrs, defStyleAttr, 0);
157 }
158
159
160 /**
161 * Construct a new Switch with a default style determined by the given theme
162 * attribute or style resource, overriding specific style attributes as
163 * requested.
164 *
165 * @param context The Context that will determine this widget's theming.
166 * @param attrs Specification of attributes that should deviate from the
167 * default styling.
168 * @param defStyleAttr An attribute in the current theme that contains a
169 * reference to a style resource that supplies default values for
170 * the view. Can be 0 to not look for defaults.
171 * @param defStyleRes A resource identifier of a style resource that
172 * supplies default values for the view, used only if
173 * defStyleAttr is 0 or can not be found in the theme. Can be 0
174 * to not look for defaults.
175 */
176 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
177 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800178
179 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700180
181 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800182 mTextPaint.density = res.getDisplayMetrics().density;
183 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
184
Alan Viverette617feb92013-09-09 18:09:13 -0700185 final TypedArray a = context.obtainStyledAttributes(
186 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700187 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
188 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Adam Powell12190b32010-11-28 19:07:53 -0800189 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
190 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
191 mThumbTextPadding = a.getDimensionPixelSize(
192 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
193 mSwitchMinWidth = a.getDimensionPixelSize(
194 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
195 mSwitchPadding = a.getDimensionPixelSize(
196 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700197 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800198
Alan Viverette661e6362014-05-12 10:55:37 -0700199 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800200 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
201 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700202 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800203 }
204 a.recycle();
205
Alan Viverette661e6362014-05-12 10:55:37 -0700206 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800207 mTouchSlop = config.getScaledTouchSlop();
208 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
209
210 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700211 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800212 setChecked(isChecked());
213 }
214
215 /**
216 * Sets the switch text color, size, style, hint color, and highlight color
217 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800218 *
219 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800220 */
Chet Haase150176d2011-08-26 09:54:06 -0700221 public void setSwitchTextAppearance(Context context, int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800222 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700223 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800224 com.android.internal.R.styleable.TextAppearance);
225
226 ColorStateList colors;
227 int ts;
228
229 colors = appearance.getColorStateList(com.android.internal.R.styleable.
230 TextAppearance_textColor);
231 if (colors != null) {
232 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700233 } else {
234 // If no color set in TextAppearance, default to the view's textColor
235 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800236 }
237
238 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
239 TextAppearance_textSize, 0);
240 if (ts != 0) {
241 if (ts != mTextPaint.getTextSize()) {
242 mTextPaint.setTextSize(ts);
243 requestLayout();
244 }
245 }
246
247 int typefaceIndex, styleIndex;
248
249 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
250 TextAppearance_typeface, -1);
251 styleIndex = appearance.getInt(com.android.internal.R.styleable.
252 TextAppearance_textStyle, -1);
253
254 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
255
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400256 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
257 TextAppearance_textAllCaps, false);
258 if (allCaps) {
259 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
260 mSwitchTransformationMethod.setLengthChangesAllowed(true);
261 } else {
262 mSwitchTransformationMethod = null;
263 }
264
Adam Powell12190b32010-11-28 19:07:53 -0800265 appearance.recycle();
266 }
267
268 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
269 Typeface tf = null;
270 switch (typefaceIndex) {
271 case SANS:
272 tf = Typeface.SANS_SERIF;
273 break;
274
275 case SERIF:
276 tf = Typeface.SERIF;
277 break;
278
279 case MONOSPACE:
280 tf = Typeface.MONOSPACE;
281 break;
282 }
283
284 setSwitchTypeface(tf, styleIndex);
285 }
286
287 /**
288 * Sets the typeface and style in which the text should be displayed on the
289 * switch, and turns on the fake bold and italic bits in the Paint if the
290 * Typeface that you provided does not have all the bits in the
291 * style that you specified.
292 */
293 public void setSwitchTypeface(Typeface tf, int style) {
294 if (style > 0) {
295 if (tf == null) {
296 tf = Typeface.defaultFromStyle(style);
297 } else {
298 tf = Typeface.create(tf, style);
299 }
300
301 setSwitchTypeface(tf);
302 // now compute what (if any) algorithmic styling is needed
303 int typefaceStyle = tf != null ? tf.getStyle() : 0;
304 int need = style & ~typefaceStyle;
305 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
306 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
307 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700308 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800309 mTextPaint.setTextSkewX(0);
310 setSwitchTypeface(tf);
311 }
312 }
313
314 /**
Chet Haase150176d2011-08-26 09:54:06 -0700315 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800316 * Note that not all Typeface families actually have bold and italic
317 * variants, so you may need to use
318 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
319 * that you actually want.
320 *
321 * @attr ref android.R.styleable#TextView_typeface
322 * @attr ref android.R.styleable#TextView_textStyle
323 */
324 public void setSwitchTypeface(Typeface tf) {
325 if (mTextPaint.getTypeface() != tf) {
326 mTextPaint.setTypeface(tf);
327
328 requestLayout();
329 invalidate();
330 }
331 }
332
333 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800334 * Set the amount of horizontal padding between the switch and the associated text.
335 *
336 * @param pixels Amount of padding in pixels
337 *
338 * @attr ref android.R.styleable#Switch_switchPadding
339 */
340 public void setSwitchPadding(int pixels) {
341 mSwitchPadding = pixels;
342 requestLayout();
343 }
344
345 /**
346 * Get the amount of horizontal padding between the switch and the associated text.
347 *
348 * @return Amount of padding in pixels
349 *
350 * @attr ref android.R.styleable#Switch_switchPadding
351 */
352 public int getSwitchPadding() {
353 return mSwitchPadding;
354 }
355
356 /**
357 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
358 * of this value and its measured width as determined by the switch drawables and text used.
359 *
360 * @param pixels Minimum width of the switch in pixels
361 *
362 * @attr ref android.R.styleable#Switch_switchMinWidth
363 */
364 public void setSwitchMinWidth(int pixels) {
365 mSwitchMinWidth = pixels;
366 requestLayout();
367 }
368
369 /**
370 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
371 * of this value and its measured width as determined by the switch drawables and text used.
372 *
373 * @return Minimum width of the switch in pixels
374 *
375 * @attr ref android.R.styleable#Switch_switchMinWidth
376 */
377 public int getSwitchMinWidth() {
378 return mSwitchMinWidth;
379 }
380
381 /**
382 * Set the horizontal padding around the text drawn on the switch itself.
383 *
384 * @param pixels Horizontal padding for switch thumb text in pixels
385 *
386 * @attr ref android.R.styleable#Switch_thumbTextPadding
387 */
388 public void setThumbTextPadding(int pixels) {
389 mThumbTextPadding = pixels;
390 requestLayout();
391 }
392
393 /**
394 * Get the horizontal padding around the text drawn on the switch itself.
395 *
396 * @return Horizontal padding for switch thumb text in pixels
397 *
398 * @attr ref android.R.styleable#Switch_thumbTextPadding
399 */
400 public int getThumbTextPadding() {
401 return mThumbTextPadding;
402 }
403
404 /**
405 * Set the drawable used for the track that the switch slides within.
406 *
407 * @param track Track drawable
408 *
409 * @attr ref android.R.styleable#Switch_track
410 */
411 public void setTrackDrawable(Drawable track) {
412 mTrackDrawable = track;
413 requestLayout();
414 }
415
416 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800417 * Set the drawable used for the track that the switch slides within.
418 *
Adam Powelldca510e2012-03-08 20:06:39 -0800419 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800420 *
421 * @attr ref android.R.styleable#Switch_track
422 */
423 public void setTrackResource(int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800424 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800425 }
426
427 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800428 * Get the drawable used for the track that the switch slides within.
429 *
430 * @return Track drawable
431 *
432 * @attr ref android.R.styleable#Switch_track
433 */
434 public Drawable getTrackDrawable() {
435 return mTrackDrawable;
436 }
437
438 /**
439 * Set the drawable used for the switch "thumb" - the piece that the user
440 * can physically touch and drag along the track.
441 *
442 * @param thumb Thumb drawable
443 *
444 * @attr ref android.R.styleable#Switch_thumb
445 */
446 public void setThumbDrawable(Drawable thumb) {
447 mThumbDrawable = thumb;
448 requestLayout();
449 }
450
451 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800452 * Set the drawable used for the switch "thumb" - the piece that the user
453 * can physically touch and drag along the track.
454 *
Adam Powelldca510e2012-03-08 20:06:39 -0800455 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800456 *
457 * @attr ref android.R.styleable#Switch_thumb
458 */
459 public void setThumbResource(int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800460 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800461 }
462
463 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800464 * Get the drawable used for the switch "thumb" - the piece that the user
465 * can physically touch and drag along the track.
466 *
467 * @return Thumb drawable
468 *
469 * @attr ref android.R.styleable#Switch_thumb
470 */
471 public Drawable getThumbDrawable() {
472 return mThumbDrawable;
473 }
474
475 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700476 * Specifies whether the track should be split by the thumb. When true,
477 * the thumb's optical bounds will be clipped out of the track drawable,
478 * then the thumb will be drawn into the resulting gap.
479 *
480 * @param splitTrack Whether the track should be split by the thumb
481 *
482 * @attr ref android.R.styleable#Switch_splitTrack
483 */
484 public void setSplitTrack(boolean splitTrack) {
485 mSplitTrack = splitTrack;
486 invalidate();
487 }
488
489 /**
490 * Returns whether the track should be split by the thumb.
491 *
492 * @attr ref android.R.styleable#Switch_splitTrack
493 */
494 public boolean getSplitTrack() {
495 return mSplitTrack;
496 }
497
498 /**
Chet Haase150176d2011-08-26 09:54:06 -0700499 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800500 *
501 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800502 */
503 public CharSequence getTextOn() {
504 return mTextOn;
505 }
506
507 /**
Chet Haase150176d2011-08-26 09:54:06 -0700508 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800509 *
510 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800511 */
512 public void setTextOn(CharSequence textOn) {
513 mTextOn = textOn;
514 requestLayout();
515 }
516
517 /**
Chet Haase150176d2011-08-26 09:54:06 -0700518 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800519 *
520 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800521 */
522 public CharSequence getTextOff() {
523 return mTextOff;
524 }
525
526 /**
Chet Haase150176d2011-08-26 09:54:06 -0700527 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800528 *
529 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800530 */
531 public void setTextOff(CharSequence textOff) {
532 mTextOff = textOff;
533 requestLayout();
534 }
535
536 @Override
537 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell12190b32010-11-28 19:07:53 -0800538 if (mOnLayout == null) {
539 mOnLayout = makeLayout(mTextOn);
540 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800541
Adam Powell12190b32010-11-28 19:07:53 -0800542 if (mOffLayout == null) {
543 mOffLayout = makeLayout(mTextOff);
544 }
545
546 mTrackDrawable.getPadding(mTempRect);
Alan Viverette5876ff42014-03-03 17:40:46 -0800547
Alan Viverette661e6362014-05-12 10:55:37 -0700548 final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
549 + mThumbTextPadding * 2;
550 mThumbWidth = Math.max(maxTextWidth, mThumbDrawable.getIntrinsicWidth());
551
Adam Powell12190b32010-11-28 19:07:53 -0800552 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette661e6362014-05-12 10:55:37 -0700553 2 * mThumbWidth + mTempRect.left + mTempRect.right);
Alan Viverette5876ff42014-03-03 17:40:46 -0800554 final int switchHeight = Math.max(mTrackDrawable.getIntrinsicHeight(),
555 mThumbDrawable.getIntrinsicHeight());
Adam Powell12190b32010-11-28 19:07:53 -0800556
Adam Powell12190b32010-11-28 19:07:53 -0800557
Adam Powell12190b32010-11-28 19:07:53 -0800558 mSwitchWidth = switchWidth;
559 mSwitchHeight = switchHeight;
560
561 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Adam Powell12190b32010-11-28 19:07:53 -0800562 final int measuredHeight = getMeasuredHeight();
563 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800564 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800565 }
566 }
567
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700568 @Override
569 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
570 super.onPopulateAccessibilityEvent(event);
Svetoslav Ganovb8c50e82012-09-14 11:33:39 -0700571 Layout layout = isChecked() ? mOnLayout : mOffLayout;
572 if (layout != null && !TextUtils.isEmpty(layout.getText())) {
573 event.getText().add(layout.getText());
Svetoslav Ganov76502592011-07-29 10:44:59 -0700574 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700575 }
576
Adam Powell12190b32010-11-28 19:07:53 -0800577 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400578 final CharSequence transformed = (mSwitchTransformationMethod != null)
579 ? mSwitchTransformationMethod.getTransformation(text, this)
580 : text;
581
582 return new StaticLayout(transformed, mTextPaint,
583 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800584 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
585 }
586
587 /**
588 * @return true if (x, y) is within the target area of the switch thumb
589 */
590 private boolean hitThumb(float x, float y) {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700591 // Relies on mTempRect, MUST be called first!
592 final int thumbOffset = getThumbOffset();
593
Adam Powell12190b32010-11-28 19:07:53 -0800594 mThumbDrawable.getPadding(mTempRect);
595 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700596 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800597 final int thumbRight = thumbLeft + mThumbWidth +
598 mTempRect.left + mTempRect.right + mTouchSlop;
599 final int thumbBottom = mSwitchBottom + mTouchSlop;
600 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
601 }
602
603 @Override
604 public boolean onTouchEvent(MotionEvent ev) {
605 mVelocityTracker.addMovement(ev);
606 final int action = ev.getActionMasked();
607 switch (action) {
608 case MotionEvent.ACTION_DOWN: {
609 final float x = ev.getX();
610 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700611 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800612 mTouchMode = TOUCH_MODE_DOWN;
613 mTouchX = x;
614 mTouchY = y;
615 }
616 break;
617 }
618
619 case MotionEvent.ACTION_MOVE: {
620 switch (mTouchMode) {
621 case TOUCH_MODE_IDLE:
622 // Didn't target the thumb, treat normally.
623 break;
624
625 case TOUCH_MODE_DOWN: {
626 final float x = ev.getX();
627 final float y = ev.getY();
628 if (Math.abs(x - mTouchX) > mTouchSlop ||
629 Math.abs(y - mTouchY) > mTouchSlop) {
630 mTouchMode = TOUCH_MODE_DRAGGING;
631 getParent().requestDisallowInterceptTouchEvent(true);
632 mTouchX = x;
633 mTouchY = y;
634 return true;
635 }
636 break;
637 }
638
639 case TOUCH_MODE_DRAGGING: {
640 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700641 final int thumbScrollRange = getThumbScrollRange();
642 final float thumbScrollOffset = x - mTouchX;
643 float dPos;
644 if (thumbScrollRange != 0) {
645 dPos = thumbScrollOffset / thumbScrollRange;
646 } else {
647 // If the thumb scroll range is empty, just use the
648 // movement direction to snap on or off.
649 dPos = thumbScrollOffset > 0 ? 1 : -1;
650 }
651 if (isLayoutRtl()) {
652 dPos = -dPos;
653 }
654 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800655 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800656 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700657 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800658 }
659 return true;
660 }
661 }
662 break;
663 }
664
665 case MotionEvent.ACTION_UP:
666 case MotionEvent.ACTION_CANCEL: {
667 if (mTouchMode == TOUCH_MODE_DRAGGING) {
668 stopDrag(ev);
669 return true;
670 }
671 mTouchMode = TOUCH_MODE_IDLE;
672 mVelocityTracker.clear();
673 break;
674 }
675 }
676
677 return super.onTouchEvent(ev);
678 }
679
680 private void cancelSuperTouch(MotionEvent ev) {
681 MotionEvent cancel = MotionEvent.obtain(ev);
682 cancel.setAction(MotionEvent.ACTION_CANCEL);
683 super.onTouchEvent(cancel);
684 cancel.recycle();
685 }
686
687 /**
688 * Called from onTouchEvent to end a drag operation.
689 *
690 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
691 */
692 private void stopDrag(MotionEvent ev) {
693 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -0800694
Alan Viverette86453ff2013-09-26 14:46:08 -0700695 // Commit the change if the event is up and not canceled and the switch
696 // has not been disabled during the drag.
697 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
698 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -0800699 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -0800700 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -0700701 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -0800702 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700703 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -0800704 } else {
705 newState = getTargetCheckedState();
706 }
Adam Powell12190b32010-11-28 19:07:53 -0800707 } else {
Alan Viverette86453ff2013-09-26 14:46:08 -0700708 newState = isChecked();
Adam Powell12190b32010-11-28 19:07:53 -0800709 }
Alan Viverette86453ff2013-09-26 14:46:08 -0700710
711 setChecked(newState);
712 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800713 }
714
715 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700716 final float targetPosition = newCheckedState ? 1 : 0;
717 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
718 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
719 mPositionAnimator.setAutoCancel(true);
720 mPositionAnimator.start();
721 }
722
723 private void cancelPositionAnimator() {
724 if (mPositionAnimator != null) {
725 mPositionAnimator.cancel();
726 }
Adam Powell12190b32010-11-28 19:07:53 -0800727 }
728
729 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700730 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700731 }
732
Alan Viverettecc2688d2013-09-17 17:00:12 -0700733 /**
734 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
735 *
736 * @param position new position between [0,1]
737 */
738 private void setThumbPosition(float position) {
739 mThumbPosition = position;
740 invalidate();
741 }
742
743 @Override
744 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -0700745 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -0800746 }
747
748 @Override
749 public void setChecked(boolean checked) {
750 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -0700751
Alan Viverette86453ff2013-09-26 14:46:08 -0700752 if (isAttachedToWindow() && isLaidOut()) {
753 animateThumbToCheckedState(checked);
754 } else {
755 // Immediately move the thumb to the new position.
756 cancelPositionAnimator();
757 setThumbPosition(checked ? 1 : 0);
758 }
Adam Powell12190b32010-11-28 19:07:53 -0800759 }
760
761 @Override
762 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
763 super.onLayout(changed, left, top, right, bottom);
764
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700765 int switchRight;
766 int switchLeft;
767
768 if (isLayoutRtl()) {
769 switchLeft = getPaddingLeft();
770 switchRight = switchLeft + mSwitchWidth;
771 } else {
772 switchRight = getWidth() - getPaddingRight();
773 switchLeft = switchRight - mSwitchWidth;
774 }
775
Adam Powell12190b32010-11-28 19:07:53 -0800776 int switchTop = 0;
777 int switchBottom = 0;
778 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
779 default:
780 case Gravity.TOP:
781 switchTop = getPaddingTop();
782 switchBottom = switchTop + mSwitchHeight;
783 break;
784
785 case Gravity.CENTER_VERTICAL:
786 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
787 mSwitchHeight / 2;
788 switchBottom = switchTop + mSwitchHeight;
789 break;
790
791 case Gravity.BOTTOM:
792 switchBottom = getHeight() - getPaddingBottom();
793 switchTop = switchBottom - mSwitchHeight;
794 break;
795 }
796
797 mSwitchLeft = switchLeft;
798 mSwitchTop = switchTop;
799 mSwitchBottom = switchBottom;
800 mSwitchRight = switchRight;
801 }
802
803 @Override
804 protected void onDraw(Canvas canvas) {
Alan Viverette5876ff42014-03-03 17:40:46 -0800805 final Rect tempRect = mTempRect;
806 final Drawable trackDrawable = mTrackDrawable;
807 final Drawable thumbDrawable = mThumbDrawable;
808
Alan Viverette661e6362014-05-12 10:55:37 -0700809 // Layout the track.
Alan Viverette5876ff42014-03-03 17:40:46 -0800810 final int switchLeft = mSwitchLeft;
811 final int switchTop = mSwitchTop;
812 final int switchRight = mSwitchRight;
813 final int switchBottom = mSwitchBottom;
814 trackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
Alan Viverette5876ff42014-03-03 17:40:46 -0800815 trackDrawable.getPadding(tempRect);
Alan Viverette61956602014-04-22 19:07:06 -0700816
Alan Viverette5876ff42014-03-03 17:40:46 -0800817 final int switchInnerLeft = switchLeft + tempRect.left;
818 final int switchInnerTop = switchTop + tempRect.top;
819 final int switchInnerRight = switchRight - tempRect.right;
820 final int switchInnerBottom = switchBottom - tempRect.bottom;
Adam Powell12190b32010-11-28 19:07:53 -0800821
Alan Viverettecc2688d2013-09-17 17:00:12 -0700822 // Relies on mTempRect, MUST be called first!
823 final int thumbPos = getThumbOffset();
824
Alan Viverette661e6362014-05-12 10:55:37 -0700825 // Layout the thumb.
Alan Viverette5876ff42014-03-03 17:40:46 -0800826 thumbDrawable.getPadding(tempRect);
Alan Viverette661e6362014-05-12 10:55:37 -0700827 final int thumbLeft = switchInnerLeft - tempRect.left + thumbPos;
828 final int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right;
Alan Viverette5876ff42014-03-03 17:40:46 -0800829 thumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -0700830
831 final Drawable background = getBackground();
Alan Viveretteaec98fe2014-04-23 12:51:16 -0700832 if (background != null && background.supportsHotspots()) {
Alan Viverette61956602014-04-22 19:07:06 -0700833 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
834 }
835
836 super.onDraw(canvas);
837
Alan Viverette661e6362014-05-12 10:55:37 -0700838 if (mSplitTrack) {
839 final Insets insets = thumbDrawable.getOpticalInsets();
840 thumbDrawable.copyBounds(tempRect);
841 tempRect.left += insets.left;
842 tempRect.right -= insets.right;
843
844 final int saveCount = canvas.save();
845 canvas.clipRect(tempRect, Op.DIFFERENCE);
846 trackDrawable.draw(canvas);
847 canvas.restoreToCount(saveCount);
848 } else {
849 trackDrawable.draw(canvas);
850 }
Alan Viverette61956602014-04-22 19:07:06 -0700851
852 final int saveCount = canvas.save();
853 canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
Alan Viverette5876ff42014-03-03 17:40:46 -0800854 thumbDrawable.draw(canvas);
Adam Powell12190b32010-11-28 19:07:53 -0800855
Alan Viverette5876ff42014-03-03 17:40:46 -0800856 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -0700857 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -0700858 final int drawableState[] = getDrawableState();
859 if (mTextColors != null) {
860 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
861 }
862 mTextPaint.drawableState = drawableState;
863
Alan Viverette5876ff42014-03-03 17:40:46 -0800864 final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2;
865 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
866 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -0700867 switchText.draw(canvas);
868 }
Adam Powell12190b32010-11-28 19:07:53 -0800869
Alan Viverette5876ff42014-03-03 17:40:46 -0800870 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -0800871 }
872
873 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700874 public int getCompoundPaddingLeft() {
875 if (!isLayoutRtl()) {
876 return super.getCompoundPaddingLeft();
877 }
878 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
879 if (!TextUtils.isEmpty(getText())) {
880 padding += mSwitchPadding;
881 }
882 return padding;
883 }
884
885 @Override
Adam Powell12190b32010-11-28 19:07:53 -0800886 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700887 if (isLayoutRtl()) {
888 return super.getCompoundPaddingRight();
889 }
Adam Powell12190b32010-11-28 19:07:53 -0800890 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
891 if (!TextUtils.isEmpty(getText())) {
892 padding += mSwitchPadding;
893 }
894 return padding;
895 }
896
Alan Viverettecc2688d2013-09-17 17:00:12 -0700897 /**
898 * Translates thumb position to offset according to current RTL setting and
899 * thumb scroll range.
900 *
901 * @return thumb offset
902 */
903 private int getThumbOffset() {
904 final float thumbPosition;
905 if (isLayoutRtl()) {
906 thumbPosition = 1 - mThumbPosition;
907 } else {
908 thumbPosition = mThumbPosition;
909 }
910 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
911 }
912
Adam Powell12190b32010-11-28 19:07:53 -0800913 private int getThumbScrollRange() {
914 if (mTrackDrawable == null) {
915 return 0;
916 }
917 mTrackDrawable.getPadding(mTempRect);
918 return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
919 }
920
921 @Override
922 protected int[] onCreateDrawableState(int extraSpace) {
923 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
924 if (isChecked()) {
925 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
926 }
927 return drawableState;
928 }
929
930 @Override
931 protected void drawableStateChanged() {
932 super.drawableStateChanged();
933
Alan Viverette661e6362014-05-12 10:55:37 -0700934 final int[] myDrawableState = getDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800935
Alan Viverette661e6362014-05-12 10:55:37 -0700936 if (mThumbDrawable != null && mThumbDrawable.setState(myDrawableState)) {
937 // Handle changes to thumb width and height.
938 requestLayout();
939 }
940
941 if (mTrackDrawable != null) {
942 mTrackDrawable.setState(myDrawableState);
943 }
Adam Powell12190b32010-11-28 19:07:53 -0800944
945 invalidate();
946 }
947
948 @Override
949 protected boolean verifyDrawable(Drawable who) {
950 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
951 }
952
953 @Override
954 public void jumpDrawablesToCurrentState() {
955 super.jumpDrawablesToCurrentState();
956 mThumbDrawable.jumpToCurrentState();
957 mTrackDrawable.jumpToCurrentState();
958 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800959
960 @Override
961 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
962 super.onInitializeAccessibilityEvent(event);
963 event.setClassName(Switch.class.getName());
964 }
965
966 @Override
967 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
968 super.onInitializeAccessibilityNodeInfo(info);
969 info.setClassName(Switch.class.getName());
Svetoslav Ganov78bcc152012-04-12 17:17:19 -0700970 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
971 if (!TextUtils.isEmpty(switchText)) {
972 CharSequence oldText = info.getText();
973 if (TextUtils.isEmpty(oldText)) {
974 info.setText(switchText);
975 } else {
976 StringBuilder newText = new StringBuilder();
977 newText.append(oldText).append(' ').append(switchText);
978 info.setText(newText);
979 }
980 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800981 }
Alan Viverettecc2688d2013-09-17 17:00:12 -0700982
983 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
984 @Override
985 public Float get(Switch object) {
986 return object.mThumbPosition;
987 }
988
989 @Override
990 public void setValue(Switch object, float value) {
991 object.setThumbPosition(value);
992 }
993 };
Adam Powell12190b32010-11-28 19:07:53 -0800994}