blob: 3d23e4d4eaf8a94282cde666eff5f9fdc649407c [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;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.Typeface;
28import android.graphics.drawable.Drawable;
29import android.text.Layout;
30import android.text.StaticLayout;
31import android.text.TextPaint;
32import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040033import android.text.method.AllCapsTransformationMethod;
34import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080035import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070036import android.util.FloatProperty;
37import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080038import android.view.Gravity;
39import android.view.MotionEvent;
40import android.view.VelocityTracker;
41import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070042import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080043import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080044
Adam Powellbe0a4532010-11-29 17:47:48 -080045import com.android.internal.R;
46
Adam Powell12190b32010-11-28 19:07:53 -080047/**
48 * A Switch is a two-state toggle switch widget that can select between two
49 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070050 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
51 * property controls the text displayed in the label for the switch, whereas the
52 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
53 * controls the text on the thumb. Similarly, the
54 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
55 * setTypeface() methods control the typeface and style of label text, whereas the
56 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
57 * the related seSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080058 *
Scott Main4c359b72012-07-24 15:51:27 -070059 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
60 * guide.</p>
61 *
62 * @attr ref android.R.styleable#Switch_textOn
63 * @attr ref android.R.styleable#Switch_textOff
64 * @attr ref android.R.styleable#Switch_switchMinWidth
65 * @attr ref android.R.styleable#Switch_switchPadding
66 * @attr ref android.R.styleable#Switch_switchTextAppearance
67 * @attr ref android.R.styleable#Switch_thumb
68 * @attr ref android.R.styleable#Switch_thumbTextPadding
69 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080070 */
71public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070072 private static final int THUMB_ANIMATION_DURATION = 250;
73
Adam Powell12190b32010-11-28 19:07:53 -080074 private static final int TOUCH_MODE_IDLE = 0;
75 private static final int TOUCH_MODE_DOWN = 1;
76 private static final int TOUCH_MODE_DRAGGING = 2;
77
78 // Enum for the "typeface" XML parameter.
79 private static final int SANS = 1;
80 private static final int SERIF = 2;
81 private static final int MONOSPACE = 3;
82
83 private Drawable mThumbDrawable;
84 private Drawable mTrackDrawable;
85 private int mThumbTextPadding;
86 private int mSwitchMinWidth;
87 private int mSwitchPadding;
88 private CharSequence mTextOn;
89 private CharSequence mTextOff;
90
91 private int mTouchMode;
92 private int mTouchSlop;
93 private float mTouchX;
94 private float mTouchY;
95 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
96 private int mMinFlingVelocity;
97
98 private float mThumbPosition;
99 private int mSwitchWidth;
100 private int mSwitchHeight;
101 private int mThumbWidth; // Does not include padding
102
103 private int mSwitchLeft;
104 private int mSwitchTop;
105 private int mSwitchRight;
106 private int mSwitchBottom;
107
108 private TextPaint mTextPaint;
109 private ColorStateList mTextColors;
110 private Layout mOnLayout;
111 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400112 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700113 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800114
Adam Powellbe0a4532010-11-29 17:47:48 -0800115 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800116 private final Rect mTempRect = new Rect();
117
118 private static final int[] CHECKED_STATE_SET = {
119 R.attr.state_checked
120 };
121
122 /**
123 * Construct a new Switch with default styling.
124 *
125 * @param context The Context that will determine this widget's theming.
126 */
127 public Switch(Context context) {
128 this(context, null);
129 }
130
131 /**
132 * Construct a new Switch with default styling, overriding specific style
133 * attributes as requested.
134 *
135 * @param context The Context that will determine this widget's theming.
136 * @param attrs Specification of attributes that should deviate from default styling.
137 */
138 public Switch(Context context, AttributeSet attrs) {
139 this(context, attrs, com.android.internal.R.attr.switchStyle);
140 }
141
142 /**
143 * Construct a new Switch with a default style determined by the given theme attribute,
144 * overriding specific style attributes as requested.
145 *
146 * @param context The Context that will determine this widget's theming.
147 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700148 * @param defStyleAttr An attribute in the current theme that contains a
149 * reference to a style resource that supplies default values for
150 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800151 */
Alan Viverette617feb92013-09-09 18:09:13 -0700152 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
153 this(context, attrs, defStyleAttr, 0);
154 }
155
156
157 /**
158 * Construct a new Switch with a default style determined by the given theme
159 * attribute or style resource, overriding specific style attributes as
160 * requested.
161 *
162 * @param context The Context that will determine this widget's theming.
163 * @param attrs Specification of attributes that should deviate from the
164 * default styling.
165 * @param defStyleAttr An attribute in the current theme that contains a
166 * reference to a style resource that supplies default values for
167 * the view. Can be 0 to not look for defaults.
168 * @param defStyleRes A resource identifier of a style resource that
169 * supplies default values for the view, used only if
170 * defStyleAttr is 0 or can not be found in the theme. Can be 0
171 * to not look for defaults.
172 */
173 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
174 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800175
176 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
177 Resources res = getResources();
178 mTextPaint.density = res.getDisplayMetrics().density;
179 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
180
Alan Viverette617feb92013-09-09 18:09:13 -0700181 final TypedArray a = context.obtainStyledAttributes(
182 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800183
Chet Haase150176d2011-08-26 09:54:06 -0700184 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
185 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Adam Powell12190b32010-11-28 19:07:53 -0800186 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
187 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
188 mThumbTextPadding = a.getDimensionPixelSize(
189 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
190 mSwitchMinWidth = a.getDimensionPixelSize(
191 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
192 mSwitchPadding = a.getDimensionPixelSize(
193 com.android.internal.R.styleable.Switch_switchPadding, 0);
194
195 int appearance = a.getResourceId(
196 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
197 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700198 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800199 }
200 a.recycle();
201
202 ViewConfiguration config = ViewConfiguration.get(context);
203 mTouchSlop = config.getScaledTouchSlop();
204 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
205
206 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700207 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800208 setChecked(isChecked());
209 }
210
211 /**
212 * Sets the switch text color, size, style, hint color, and highlight color
213 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800214 *
215 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800216 */
Chet Haase150176d2011-08-26 09:54:06 -0700217 public void setSwitchTextAppearance(Context context, int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800218 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700219 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800220 com.android.internal.R.styleable.TextAppearance);
221
222 ColorStateList colors;
223 int ts;
224
225 colors = appearance.getColorStateList(com.android.internal.R.styleable.
226 TextAppearance_textColor);
227 if (colors != null) {
228 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700229 } else {
230 // If no color set in TextAppearance, default to the view's textColor
231 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800232 }
233
234 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
235 TextAppearance_textSize, 0);
236 if (ts != 0) {
237 if (ts != mTextPaint.getTextSize()) {
238 mTextPaint.setTextSize(ts);
239 requestLayout();
240 }
241 }
242
243 int typefaceIndex, styleIndex;
244
245 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
246 TextAppearance_typeface, -1);
247 styleIndex = appearance.getInt(com.android.internal.R.styleable.
248 TextAppearance_textStyle, -1);
249
250 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
251
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400252 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
253 TextAppearance_textAllCaps, false);
254 if (allCaps) {
255 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
256 mSwitchTransformationMethod.setLengthChangesAllowed(true);
257 } else {
258 mSwitchTransformationMethod = null;
259 }
260
Adam Powell12190b32010-11-28 19:07:53 -0800261 appearance.recycle();
262 }
263
264 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
265 Typeface tf = null;
266 switch (typefaceIndex) {
267 case SANS:
268 tf = Typeface.SANS_SERIF;
269 break;
270
271 case SERIF:
272 tf = Typeface.SERIF;
273 break;
274
275 case MONOSPACE:
276 tf = Typeface.MONOSPACE;
277 break;
278 }
279
280 setSwitchTypeface(tf, styleIndex);
281 }
282
283 /**
284 * Sets the typeface and style in which the text should be displayed on the
285 * switch, and turns on the fake bold and italic bits in the Paint if the
286 * Typeface that you provided does not have all the bits in the
287 * style that you specified.
288 */
289 public void setSwitchTypeface(Typeface tf, int style) {
290 if (style > 0) {
291 if (tf == null) {
292 tf = Typeface.defaultFromStyle(style);
293 } else {
294 tf = Typeface.create(tf, style);
295 }
296
297 setSwitchTypeface(tf);
298 // now compute what (if any) algorithmic styling is needed
299 int typefaceStyle = tf != null ? tf.getStyle() : 0;
300 int need = style & ~typefaceStyle;
301 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
302 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
303 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700304 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800305 mTextPaint.setTextSkewX(0);
306 setSwitchTypeface(tf);
307 }
308 }
309
310 /**
Chet Haase150176d2011-08-26 09:54:06 -0700311 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800312 * Note that not all Typeface families actually have bold and italic
313 * variants, so you may need to use
314 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
315 * that you actually want.
316 *
317 * @attr ref android.R.styleable#TextView_typeface
318 * @attr ref android.R.styleable#TextView_textStyle
319 */
320 public void setSwitchTypeface(Typeface tf) {
321 if (mTextPaint.getTypeface() != tf) {
322 mTextPaint.setTypeface(tf);
323
324 requestLayout();
325 invalidate();
326 }
327 }
328
329 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800330 * Set the amount of horizontal padding between the switch and the associated text.
331 *
332 * @param pixels Amount of padding in pixels
333 *
334 * @attr ref android.R.styleable#Switch_switchPadding
335 */
336 public void setSwitchPadding(int pixels) {
337 mSwitchPadding = pixels;
338 requestLayout();
339 }
340
341 /**
342 * Get the amount of horizontal padding between the switch and the associated text.
343 *
344 * @return Amount of padding in pixels
345 *
346 * @attr ref android.R.styleable#Switch_switchPadding
347 */
348 public int getSwitchPadding() {
349 return mSwitchPadding;
350 }
351
352 /**
353 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
354 * of this value and its measured width as determined by the switch drawables and text used.
355 *
356 * @param pixels Minimum width of the switch in pixels
357 *
358 * @attr ref android.R.styleable#Switch_switchMinWidth
359 */
360 public void setSwitchMinWidth(int pixels) {
361 mSwitchMinWidth = pixels;
362 requestLayout();
363 }
364
365 /**
366 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
367 * of this value and its measured width as determined by the switch drawables and text used.
368 *
369 * @return Minimum width of the switch in pixels
370 *
371 * @attr ref android.R.styleable#Switch_switchMinWidth
372 */
373 public int getSwitchMinWidth() {
374 return mSwitchMinWidth;
375 }
376
377 /**
378 * Set the horizontal padding around the text drawn on the switch itself.
379 *
380 * @param pixels Horizontal padding for switch thumb text in pixels
381 *
382 * @attr ref android.R.styleable#Switch_thumbTextPadding
383 */
384 public void setThumbTextPadding(int pixels) {
385 mThumbTextPadding = pixels;
386 requestLayout();
387 }
388
389 /**
390 * Get the horizontal padding around the text drawn on the switch itself.
391 *
392 * @return Horizontal padding for switch thumb text in pixels
393 *
394 * @attr ref android.R.styleable#Switch_thumbTextPadding
395 */
396 public int getThumbTextPadding() {
397 return mThumbTextPadding;
398 }
399
400 /**
401 * Set the drawable used for the track that the switch slides within.
402 *
403 * @param track Track drawable
404 *
405 * @attr ref android.R.styleable#Switch_track
406 */
407 public void setTrackDrawable(Drawable track) {
408 mTrackDrawable = track;
409 requestLayout();
410 }
411
412 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800413 * Set the drawable used for the track that the switch slides within.
414 *
Adam Powelldca510e2012-03-08 20:06:39 -0800415 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800416 *
417 * @attr ref android.R.styleable#Switch_track
418 */
419 public void setTrackResource(int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800420 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800421 }
422
423 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800424 * Get the drawable used for the track that the switch slides within.
425 *
426 * @return Track drawable
427 *
428 * @attr ref android.R.styleable#Switch_track
429 */
430 public Drawable getTrackDrawable() {
431 return mTrackDrawable;
432 }
433
434 /**
435 * Set the drawable used for the switch "thumb" - the piece that the user
436 * can physically touch and drag along the track.
437 *
438 * @param thumb Thumb drawable
439 *
440 * @attr ref android.R.styleable#Switch_thumb
441 */
442 public void setThumbDrawable(Drawable thumb) {
443 mThumbDrawable = thumb;
444 requestLayout();
445 }
446
447 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800448 * Set the drawable used for the switch "thumb" - the piece that the user
449 * can physically touch and drag along the track.
450 *
Adam Powelldca510e2012-03-08 20:06:39 -0800451 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800452 *
453 * @attr ref android.R.styleable#Switch_thumb
454 */
455 public void setThumbResource(int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800456 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800457 }
458
459 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800460 * Get the drawable used for the switch "thumb" - the piece that the user
461 * can physically touch and drag along the track.
462 *
463 * @return Thumb drawable
464 *
465 * @attr ref android.R.styleable#Switch_thumb
466 */
467 public Drawable getThumbDrawable() {
468 return mThumbDrawable;
469 }
470
471 /**
Chet Haase150176d2011-08-26 09:54:06 -0700472 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800473 *
474 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800475 */
476 public CharSequence getTextOn() {
477 return mTextOn;
478 }
479
480 /**
Chet Haase150176d2011-08-26 09:54:06 -0700481 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800482 *
483 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800484 */
485 public void setTextOn(CharSequence textOn) {
486 mTextOn = textOn;
487 requestLayout();
488 }
489
490 /**
Chet Haase150176d2011-08-26 09:54:06 -0700491 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800492 *
493 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800494 */
495 public CharSequence getTextOff() {
496 return mTextOff;
497 }
498
499 /**
Chet Haase150176d2011-08-26 09:54:06 -0700500 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800501 *
502 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800503 */
504 public void setTextOff(CharSequence textOff) {
505 mTextOff = textOff;
506 requestLayout();
507 }
508
509 @Override
510 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell12190b32010-11-28 19:07:53 -0800511 if (mOnLayout == null) {
512 mOnLayout = makeLayout(mTextOn);
513 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800514
Adam Powell12190b32010-11-28 19:07:53 -0800515 if (mOffLayout == null) {
516 mOffLayout = makeLayout(mTextOff);
517 }
518
519 mTrackDrawable.getPadding(mTempRect);
Alan Viverette5876ff42014-03-03 17:40:46 -0800520
Adam Powell12190b32010-11-28 19:07:53 -0800521 final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth());
522 final int switchWidth = Math.max(mSwitchMinWidth,
523 maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
Alan Viverette5876ff42014-03-03 17:40:46 -0800524 final int switchHeight = Math.max(mTrackDrawable.getIntrinsicHeight(),
525 mThumbDrawable.getIntrinsicHeight());
Adam Powell12190b32010-11-28 19:07:53 -0800526
527 mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
528
Adam Powell12190b32010-11-28 19:07:53 -0800529 mSwitchWidth = switchWidth;
530 mSwitchHeight = switchHeight;
531
532 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Adam Powell12190b32010-11-28 19:07:53 -0800533 final int measuredHeight = getMeasuredHeight();
534 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800535 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800536 }
537 }
538
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700539 @Override
540 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
541 super.onPopulateAccessibilityEvent(event);
Svetoslav Ganovb8c50e82012-09-14 11:33:39 -0700542 Layout layout = isChecked() ? mOnLayout : mOffLayout;
543 if (layout != null && !TextUtils.isEmpty(layout.getText())) {
544 event.getText().add(layout.getText());
Svetoslav Ganov76502592011-07-29 10:44:59 -0700545 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700546 }
547
Adam Powell12190b32010-11-28 19:07:53 -0800548 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400549 final CharSequence transformed = (mSwitchTransformationMethod != null)
550 ? mSwitchTransformationMethod.getTransformation(text, this)
551 : text;
552
553 return new StaticLayout(transformed, mTextPaint,
554 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)),
Adam Powell12190b32010-11-28 19:07:53 -0800555 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
556 }
557
558 /**
559 * @return true if (x, y) is within the target area of the switch thumb
560 */
561 private boolean hitThumb(float x, float y) {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700562 // Relies on mTempRect, MUST be called first!
563 final int thumbOffset = getThumbOffset();
564
Adam Powell12190b32010-11-28 19:07:53 -0800565 mThumbDrawable.getPadding(mTempRect);
566 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700567 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800568 final int thumbRight = thumbLeft + mThumbWidth +
569 mTempRect.left + mTempRect.right + mTouchSlop;
570 final int thumbBottom = mSwitchBottom + mTouchSlop;
571 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
572 }
573
574 @Override
575 public boolean onTouchEvent(MotionEvent ev) {
576 mVelocityTracker.addMovement(ev);
577 final int action = ev.getActionMasked();
578 switch (action) {
579 case MotionEvent.ACTION_DOWN: {
580 final float x = ev.getX();
581 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700582 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800583 mTouchMode = TOUCH_MODE_DOWN;
584 mTouchX = x;
585 mTouchY = y;
586 }
587 break;
588 }
589
590 case MotionEvent.ACTION_MOVE: {
591 switch (mTouchMode) {
592 case TOUCH_MODE_IDLE:
593 // Didn't target the thumb, treat normally.
594 break;
595
596 case TOUCH_MODE_DOWN: {
597 final float x = ev.getX();
598 final float y = ev.getY();
599 if (Math.abs(x - mTouchX) > mTouchSlop ||
600 Math.abs(y - mTouchY) > mTouchSlop) {
601 mTouchMode = TOUCH_MODE_DRAGGING;
602 getParent().requestDisallowInterceptTouchEvent(true);
603 mTouchX = x;
604 mTouchY = y;
605 return true;
606 }
607 break;
608 }
609
610 case TOUCH_MODE_DRAGGING: {
611 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700612 final int thumbScrollRange = getThumbScrollRange();
613 final float thumbScrollOffset = x - mTouchX;
614 float dPos;
615 if (thumbScrollRange != 0) {
616 dPos = thumbScrollOffset / thumbScrollRange;
617 } else {
618 // If the thumb scroll range is empty, just use the
619 // movement direction to snap on or off.
620 dPos = thumbScrollOffset > 0 ? 1 : -1;
621 }
622 if (isLayoutRtl()) {
623 dPos = -dPos;
624 }
625 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800626 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800627 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700628 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800629 }
630 return true;
631 }
632 }
633 break;
634 }
635
636 case MotionEvent.ACTION_UP:
637 case MotionEvent.ACTION_CANCEL: {
638 if (mTouchMode == TOUCH_MODE_DRAGGING) {
639 stopDrag(ev);
640 return true;
641 }
642 mTouchMode = TOUCH_MODE_IDLE;
643 mVelocityTracker.clear();
644 break;
645 }
646 }
647
648 return super.onTouchEvent(ev);
649 }
650
651 private void cancelSuperTouch(MotionEvent ev) {
652 MotionEvent cancel = MotionEvent.obtain(ev);
653 cancel.setAction(MotionEvent.ACTION_CANCEL);
654 super.onTouchEvent(cancel);
655 cancel.recycle();
656 }
657
658 /**
659 * Called from onTouchEvent to end a drag operation.
660 *
661 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
662 */
663 private void stopDrag(MotionEvent ev) {
664 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -0800665
Alan Viverette86453ff2013-09-26 14:46:08 -0700666 // Commit the change if the event is up and not canceled and the switch
667 // has not been disabled during the drag.
668 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
669 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -0800670 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -0800671 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -0700672 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -0800673 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700674 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -0800675 } else {
676 newState = getTargetCheckedState();
677 }
Adam Powell12190b32010-11-28 19:07:53 -0800678 } else {
Alan Viverette86453ff2013-09-26 14:46:08 -0700679 newState = isChecked();
Adam Powell12190b32010-11-28 19:07:53 -0800680 }
Alan Viverette86453ff2013-09-26 14:46:08 -0700681
682 setChecked(newState);
683 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -0800684 }
685
686 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700687 final float targetPosition = newCheckedState ? 1 : 0;
688 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
689 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
690 mPositionAnimator.setAutoCancel(true);
691 mPositionAnimator.start();
692 }
693
694 private void cancelPositionAnimator() {
695 if (mPositionAnimator != null) {
696 mPositionAnimator.cancel();
697 }
Adam Powell12190b32010-11-28 19:07:53 -0800698 }
699
700 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -0700701 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700702 }
703
Alan Viverettecc2688d2013-09-17 17:00:12 -0700704 /**
705 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
706 *
707 * @param position new position between [0,1]
708 */
709 private void setThumbPosition(float position) {
710 mThumbPosition = position;
711 invalidate();
712 }
713
714 @Override
715 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -0700716 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -0800717 }
718
719 @Override
720 public void setChecked(boolean checked) {
721 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -0700722
Alan Viverette86453ff2013-09-26 14:46:08 -0700723 if (isAttachedToWindow() && isLaidOut()) {
724 animateThumbToCheckedState(checked);
725 } else {
726 // Immediately move the thumb to the new position.
727 cancelPositionAnimator();
728 setThumbPosition(checked ? 1 : 0);
729 }
Adam Powell12190b32010-11-28 19:07:53 -0800730 }
731
732 @Override
733 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
734 super.onLayout(changed, left, top, right, bottom);
735
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700736 int switchRight;
737 int switchLeft;
738
739 if (isLayoutRtl()) {
740 switchLeft = getPaddingLeft();
741 switchRight = switchLeft + mSwitchWidth;
742 } else {
743 switchRight = getWidth() - getPaddingRight();
744 switchLeft = switchRight - mSwitchWidth;
745 }
746
Adam Powell12190b32010-11-28 19:07:53 -0800747 int switchTop = 0;
748 int switchBottom = 0;
749 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
750 default:
751 case Gravity.TOP:
752 switchTop = getPaddingTop();
753 switchBottom = switchTop + mSwitchHeight;
754 break;
755
756 case Gravity.CENTER_VERTICAL:
757 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
758 mSwitchHeight / 2;
759 switchBottom = switchTop + mSwitchHeight;
760 break;
761
762 case Gravity.BOTTOM:
763 switchBottom = getHeight() - getPaddingBottom();
764 switchTop = switchBottom - mSwitchHeight;
765 break;
766 }
767
768 mSwitchLeft = switchLeft;
769 mSwitchTop = switchTop;
770 mSwitchBottom = switchBottom;
771 mSwitchRight = switchRight;
772 }
773
774 @Override
775 protected void onDraw(Canvas canvas) {
776 super.onDraw(canvas);
777
Alan Viverette5876ff42014-03-03 17:40:46 -0800778 final Rect tempRect = mTempRect;
779 final Drawable trackDrawable = mTrackDrawable;
780 final Drawable thumbDrawable = mThumbDrawable;
781
Adam Powell12190b32010-11-28 19:07:53 -0800782 // Draw the switch
Alan Viverette5876ff42014-03-03 17:40:46 -0800783 final int switchLeft = mSwitchLeft;
784 final int switchTop = mSwitchTop;
785 final int switchRight = mSwitchRight;
786 final int switchBottom = mSwitchBottom;
787 trackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
788 trackDrawable.draw(canvas);
Adam Powell12190b32010-11-28 19:07:53 -0800789
Alan Viverette5876ff42014-03-03 17:40:46 -0800790 final int saveCount = canvas.save();
Adam Powell12190b32010-11-28 19:07:53 -0800791
Alan Viverette5876ff42014-03-03 17:40:46 -0800792 trackDrawable.getPadding(tempRect);
793 final int switchInnerLeft = switchLeft + tempRect.left;
794 final int switchInnerTop = switchTop + tempRect.top;
795 final int switchInnerRight = switchRight - tempRect.right;
796 final int switchInnerBottom = switchBottom - tempRect.bottom;
Adam Powell12190b32010-11-28 19:07:53 -0800797 canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
798
Alan Viverettecc2688d2013-09-17 17:00:12 -0700799 // Relies on mTempRect, MUST be called first!
800 final int thumbPos = getThumbOffset();
801
Alan Viverette5876ff42014-03-03 17:40:46 -0800802 thumbDrawable.getPadding(tempRect);
803 int thumbLeft = switchInnerLeft - tempRect.left + thumbPos;
804 int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right;
805 thumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
806 thumbDrawable.draw(canvas);
Adam Powell12190b32010-11-28 19:07:53 -0800807
Alan Viverette5876ff42014-03-03 17:40:46 -0800808 final int drawableState[] = getDrawableState();
Chet Haase150176d2011-08-26 09:54:06 -0700809 if (mTextColors != null) {
Alan Viverette5876ff42014-03-03 17:40:46 -0800810 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
Chet Haase150176d2011-08-26 09:54:06 -0700811 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800812 mTextPaint.drawableState = drawableState;
Adam Powell12190b32010-11-28 19:07:53 -0800813
Alan Viverette5876ff42014-03-03 17:40:46 -0800814 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -0700815 if (switchText != null) {
Alan Viverette5876ff42014-03-03 17:40:46 -0800816 final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2;
817 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
818 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -0700819 switchText.draw(canvas);
820 }
Adam Powell12190b32010-11-28 19:07:53 -0800821
Alan Viverette5876ff42014-03-03 17:40:46 -0800822 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -0800823 }
824
825 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700826 public int getCompoundPaddingLeft() {
827 if (!isLayoutRtl()) {
828 return super.getCompoundPaddingLeft();
829 }
830 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
831 if (!TextUtils.isEmpty(getText())) {
832 padding += mSwitchPadding;
833 }
834 return padding;
835 }
836
837 @Override
Adam Powell12190b32010-11-28 19:07:53 -0800838 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -0700839 if (isLayoutRtl()) {
840 return super.getCompoundPaddingRight();
841 }
Adam Powell12190b32010-11-28 19:07:53 -0800842 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
843 if (!TextUtils.isEmpty(getText())) {
844 padding += mSwitchPadding;
845 }
846 return padding;
847 }
848
Alan Viverettecc2688d2013-09-17 17:00:12 -0700849 /**
850 * Translates thumb position to offset according to current RTL setting and
851 * thumb scroll range.
852 *
853 * @return thumb offset
854 */
855 private int getThumbOffset() {
856 final float thumbPosition;
857 if (isLayoutRtl()) {
858 thumbPosition = 1 - mThumbPosition;
859 } else {
860 thumbPosition = mThumbPosition;
861 }
862 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
863 }
864
Adam Powell12190b32010-11-28 19:07:53 -0800865 private int getThumbScrollRange() {
866 if (mTrackDrawable == null) {
867 return 0;
868 }
869 mTrackDrawable.getPadding(mTempRect);
870 return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
871 }
872
873 @Override
874 protected int[] onCreateDrawableState(int extraSpace) {
875 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
876 if (isChecked()) {
877 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
878 }
879 return drawableState;
880 }
881
882 @Override
883 protected void drawableStateChanged() {
884 super.drawableStateChanged();
885
886 int[] myDrawableState = getDrawableState();
887
888 // Set the state of the Drawable
Gilles Debunnee724ee42011-08-31 11:20:27 -0700889 // Drawable may be null when checked state is set from XML, from super constructor
890 if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
891 if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
Adam Powell12190b32010-11-28 19:07:53 -0800892
893 invalidate();
894 }
895
896 @Override
897 protected boolean verifyDrawable(Drawable who) {
898 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
899 }
900
901 @Override
902 public void jumpDrawablesToCurrentState() {
903 super.jumpDrawablesToCurrentState();
904 mThumbDrawable.jumpToCurrentState();
905 mTrackDrawable.jumpToCurrentState();
906 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800907
908 @Override
909 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
910 super.onInitializeAccessibilityEvent(event);
911 event.setClassName(Switch.class.getName());
912 }
913
914 @Override
915 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
916 super.onInitializeAccessibilityNodeInfo(info);
917 info.setClassName(Switch.class.getName());
Svetoslav Ganov78bcc152012-04-12 17:17:19 -0700918 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
919 if (!TextUtils.isEmpty(switchText)) {
920 CharSequence oldText = info.getText();
921 if (TextUtils.isEmpty(oldText)) {
922 info.setText(switchText);
923 } else {
924 StringBuilder newText = new StringBuilder();
925 newText.append(oldText).append(' ').append(switchText);
926 info.setText(newText);
927 }
928 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800929 }
Alan Viverettecc2688d2013-09-17 17:00:12 -0700930
931 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
932 @Override
933 public Float get(Switch object) {
934 return object.mThumbPosition;
935 }
936
937 @Override
938 public void setValue(Switch object, float value) {
939 object.setThumbPosition(value);
940 }
941 };
Adam Powell12190b32010-11-28 19:07:53 -0800942}