blob: cca29cf39db80a5800885ce8964404b31fc60155 [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);
Alan Viverettead2f8e32014-05-16 13:28:33 -0700669 // Allow super class to handle pressed state, etc.
670 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800671 return true;
672 }
673 mTouchMode = TOUCH_MODE_IDLE;
674 mVelocityTracker.clear();
675 break;
676 }
677 }
678
679 return super.onTouchEvent(ev);
680 }
681
682 private void cancelSuperTouch(MotionEvent ev) {
683 MotionEvent cancel = MotionEvent.obtain(ev);
684 cancel.setAction(MotionEvent.ACTION_CANCEL);
685 super.onTouchEvent(cancel);
686 cancel.recycle();
687 }
688
689 /**
690 * Called from onTouchEvent to end a drag operation.
691 *
692 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
693 */
694 private void stopDrag(MotionEvent ev) {
695 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -0800696
Alan Viverette86453ff2013-09-26 14:46:08 -0700697 // Commit the change if the event is up and not canceled and the switch
698 // has not been disabled during the drag.
699 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
700 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -0800701 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -0800702 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -0700703 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -0800704 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700705 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -0800706 } else {
707 newState = getTargetCheckedState();
708 }
Adam Powell12190b32010-11-28 19:07:53 -0800709 } else {
Alan Viverette86453ff2013-09-26 14:46:08 -0700710 newState = isChecked();
Adam Powell12190b32010-11-28 19:07:53 -0800711 }
Alan Viverette86453ff2013-09-26 14:46:08 -0700712
713 setChecked(newState);
714 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800715 }
716
717 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700718 final float targetPosition = newCheckedState ? 1 : 0;
719 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
720 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
721 mPositionAnimator.setAutoCancel(true);
722 mPositionAnimator.start();
723 }
724
725 private void cancelPositionAnimator() {
726 if (mPositionAnimator != null) {
727 mPositionAnimator.cancel();
728 }
Adam Powell12190b32010-11-28 19:07:53 -0800729 }
730
731 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700732 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700733 }
734
Alan Viverettecc2688d2013-09-17 17:00:12 -0700735 /**
736 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
737 *
738 * @param position new position between [0,1]
739 */
740 private void setThumbPosition(float position) {
741 mThumbPosition = position;
742 invalidate();
743 }
744
745 @Override
746 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -0700747 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -0800748 }
749
750 @Override
751 public void setChecked(boolean checked) {
752 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -0700753
Alan Viverette86453ff2013-09-26 14:46:08 -0700754 if (isAttachedToWindow() && isLaidOut()) {
755 animateThumbToCheckedState(checked);
756 } else {
757 // Immediately move the thumb to the new position.
758 cancelPositionAnimator();
759 setThumbPosition(checked ? 1 : 0);
760 }
Adam Powell12190b32010-11-28 19:07:53 -0800761 }
762
763 @Override
764 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
765 super.onLayout(changed, left, top, right, bottom);
766
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700767 int switchRight;
768 int switchLeft;
769
770 if (isLayoutRtl()) {
771 switchLeft = getPaddingLeft();
772 switchRight = switchLeft + mSwitchWidth;
773 } else {
774 switchRight = getWidth() - getPaddingRight();
775 switchLeft = switchRight - mSwitchWidth;
776 }
777
Adam Powell12190b32010-11-28 19:07:53 -0800778 int switchTop = 0;
779 int switchBottom = 0;
780 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
781 default:
782 case Gravity.TOP:
783 switchTop = getPaddingTop();
784 switchBottom = switchTop + mSwitchHeight;
785 break;
786
787 case Gravity.CENTER_VERTICAL:
788 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
789 mSwitchHeight / 2;
790 switchBottom = switchTop + mSwitchHeight;
791 break;
792
793 case Gravity.BOTTOM:
794 switchBottom = getHeight() - getPaddingBottom();
795 switchTop = switchBottom - mSwitchHeight;
796 break;
797 }
798
799 mSwitchLeft = switchLeft;
800 mSwitchTop = switchTop;
801 mSwitchBottom = switchBottom;
802 mSwitchRight = switchRight;
803 }
804
805 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -0700806 public void draw(Canvas c) {
Alan Viverette5876ff42014-03-03 17:40:46 -0800807 final Rect tempRect = mTempRect;
808 final Drawable trackDrawable = mTrackDrawable;
809 final Drawable thumbDrawable = mThumbDrawable;
810
Alan Viverette661e6362014-05-12 10:55:37 -0700811 // Layout the track.
Alan Viverette5876ff42014-03-03 17:40:46 -0800812 final int switchLeft = mSwitchLeft;
813 final int switchTop = mSwitchTop;
814 final int switchRight = mSwitchRight;
815 final int switchBottom = mSwitchBottom;
816 trackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
Alan Viverette5876ff42014-03-03 17:40:46 -0800817 trackDrawable.getPadding(tempRect);
Alan Viverette61956602014-04-22 19:07:06 -0700818
Alan Viverette5876ff42014-03-03 17:40:46 -0800819 final int switchInnerLeft = switchLeft + tempRect.left;
Adam Powell12190b32010-11-28 19:07:53 -0800820
Alan Viverettecc2688d2013-09-17 17:00:12 -0700821 // Relies on mTempRect, MUST be called first!
822 final int thumbPos = getThumbOffset();
823
Alan Viverette661e6362014-05-12 10:55:37 -0700824 // Layout the thumb.
Alan Viverette5876ff42014-03-03 17:40:46 -0800825 thumbDrawable.getPadding(tempRect);
Alan Viverette661e6362014-05-12 10:55:37 -0700826 final int thumbLeft = switchInnerLeft - tempRect.left + thumbPos;
827 final int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right;
Alan Viverette5876ff42014-03-03 17:40:46 -0800828 thumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -0700829
830 final Drawable background = getBackground();
Alan Viverettec80ad992014-05-19 15:46:17 -0700831 if (background != null) {
Alan Viverette61956602014-04-22 19:07:06 -0700832 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
833 }
834
Alan Viverettead2f8e32014-05-16 13:28:33 -0700835 // Draw the background.
836 super.draw(c);
837 }
838
839 @Override
840 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -0700841 super.onDraw(canvas);
842
Alan Viverettead2f8e32014-05-16 13:28:33 -0700843 final Rect tempRect = mTempRect;
844 final Drawable trackDrawable = mTrackDrawable;
845 final Drawable thumbDrawable = mThumbDrawable;
846 trackDrawable.getPadding(tempRect);
847
848 final int switchTop = mSwitchTop;
849 final int switchBottom = mSwitchBottom;
850 final int switchInnerLeft = mSwitchLeft + tempRect.left;
851 final int switchInnerTop = switchTop + tempRect.top;
852 final int switchInnerRight = mSwitchRight - tempRect.right;
853 final int switchInnerBottom = switchBottom - tempRect.bottom;
854
Alan Viverette661e6362014-05-12 10:55:37 -0700855 if (mSplitTrack) {
856 final Insets insets = thumbDrawable.getOpticalInsets();
857 thumbDrawable.copyBounds(tempRect);
858 tempRect.left += insets.left;
859 tempRect.right -= insets.right;
860
861 final int saveCount = canvas.save();
862 canvas.clipRect(tempRect, Op.DIFFERENCE);
863 trackDrawable.draw(canvas);
864 canvas.restoreToCount(saveCount);
865 } else {
866 trackDrawable.draw(canvas);
867 }
Alan Viverette61956602014-04-22 19:07:06 -0700868
869 final int saveCount = canvas.save();
870 canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
Alan Viverette5876ff42014-03-03 17:40:46 -0800871 thumbDrawable.draw(canvas);
Adam Powell12190b32010-11-28 19:07:53 -0800872
Alan Viverette5876ff42014-03-03 17:40:46 -0800873 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -0700874 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -0700875 final int drawableState[] = getDrawableState();
876 if (mTextColors != null) {
877 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
878 }
879 mTextPaint.drawableState = drawableState;
880
Alan Viverettead2f8e32014-05-16 13:28:33 -0700881 final Rect thumbBounds = thumbDrawable.getBounds();
882 final int left = (thumbBounds.left + thumbBounds.right) / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -0800883 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
884 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -0700885 switchText.draw(canvas);
886 }
Adam Powell12190b32010-11-28 19:07:53 -0800887
Alan Viverette5876ff42014-03-03 17:40:46 -0800888 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -0800889 }
890
891 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700892 public int getCompoundPaddingLeft() {
893 if (!isLayoutRtl()) {
894 return super.getCompoundPaddingLeft();
895 }
896 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
897 if (!TextUtils.isEmpty(getText())) {
898 padding += mSwitchPadding;
899 }
900 return padding;
901 }
902
903 @Override
Adam Powell12190b32010-11-28 19:07:53 -0800904 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700905 if (isLayoutRtl()) {
906 return super.getCompoundPaddingRight();
907 }
Adam Powell12190b32010-11-28 19:07:53 -0800908 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
909 if (!TextUtils.isEmpty(getText())) {
910 padding += mSwitchPadding;
911 }
912 return padding;
913 }
914
Alan Viverettecc2688d2013-09-17 17:00:12 -0700915 /**
916 * Translates thumb position to offset according to current RTL setting and
917 * thumb scroll range.
918 *
919 * @return thumb offset
920 */
921 private int getThumbOffset() {
922 final float thumbPosition;
923 if (isLayoutRtl()) {
924 thumbPosition = 1 - mThumbPosition;
925 } else {
926 thumbPosition = mThumbPosition;
927 }
928 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
929 }
930
Adam Powell12190b32010-11-28 19:07:53 -0800931 private int getThumbScrollRange() {
932 if (mTrackDrawable == null) {
933 return 0;
934 }
935 mTrackDrawable.getPadding(mTempRect);
936 return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
937 }
938
939 @Override
940 protected int[] onCreateDrawableState(int extraSpace) {
941 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
942 if (isChecked()) {
943 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
944 }
945 return drawableState;
946 }
947
948 @Override
949 protected void drawableStateChanged() {
950 super.drawableStateChanged();
951
Alan Viverette661e6362014-05-12 10:55:37 -0700952 final int[] myDrawableState = getDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800953
Alan Viverette2356c5e2014-05-22 22:43:59 -0700954 if (mThumbDrawable != null) {
955 mThumbDrawable.setState(myDrawableState);
Alan Viverette661e6362014-05-12 10:55:37 -0700956 }
957
958 if (mTrackDrawable != null) {
959 mTrackDrawable.setState(myDrawableState);
960 }
Adam Powell12190b32010-11-28 19:07:53 -0800961
962 invalidate();
963 }
964
Alan Viverettecebc6ba2014-06-13 15:52:13 -0700965 @Override
Alan Viverette8de14942014-06-18 18:05:15 -0700966 public void drawableHotspotChanged(float x, float y) {
967 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -0700968
969 if (mThumbDrawable != null) {
970 mThumbDrawable.setHotspot(x, y);
971 }
972
973 if (mTrackDrawable != null) {
974 mTrackDrawable.setHotspot(x, y);
975 }
976 }
977
Adam Powell12190b32010-11-28 19:07:53 -0800978 @Override
Alan Viverette2356c5e2014-05-22 22:43:59 -0700979 public void invalidateDrawable(Drawable drawable) {
980 super.invalidateDrawable(drawable);
981
982 if (drawable == mThumbDrawable) {
983 // Handle changes to thumb width and height.
984 requestLayout();
985 }
986 }
987
988 @Override
Adam Powell12190b32010-11-28 19:07:53 -0800989 protected boolean verifyDrawable(Drawable who) {
990 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
991 }
992
993 @Override
994 public void jumpDrawablesToCurrentState() {
995 super.jumpDrawablesToCurrentState();
996 mThumbDrawable.jumpToCurrentState();
997 mTrackDrawable.jumpToCurrentState();
998 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800999
1000 @Override
1001 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1002 super.onInitializeAccessibilityEvent(event);
1003 event.setClassName(Switch.class.getName());
1004 }
1005
1006 @Override
1007 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1008 super.onInitializeAccessibilityNodeInfo(info);
1009 info.setClassName(Switch.class.getName());
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001010 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1011 if (!TextUtils.isEmpty(switchText)) {
1012 CharSequence oldText = info.getText();
1013 if (TextUtils.isEmpty(oldText)) {
1014 info.setText(switchText);
1015 } else {
1016 StringBuilder newText = new StringBuilder();
1017 newText.append(oldText).append(' ').append(switchText);
1018 info.setText(newText);
1019 }
1020 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001021 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001022
1023 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1024 @Override
1025 public Float get(Switch object) {
1026 return object.mThumbPosition;
1027 }
1028
1029 @Override
1030 public void setValue(Switch object, float value) {
1031 object.setThumbPosition(value);
1032 }
1033 };
Adam Powell12190b32010-11-28 19:07:53 -08001034}