blob: 0c80a1170b135f3d27fc5f95135f910219a52ac0 [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
Adam Powell12190b32010-11-28 19:07:53 -080019import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.graphics.drawable.Drawable;
28import android.text.Layout;
29import android.text.StaticLayout;
30import android.text.TextPaint;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.view.Gravity;
34import android.view.MotionEvent;
35import android.view.VelocityTracker;
36import android.view.ViewConfiguration;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070037import android.view.accessibility.AccessibilityEvent;
Adam Powell12190b32010-11-28 19:07:53 -080038
Adam Powellbe0a4532010-11-29 17:47:48 -080039import com.android.internal.R;
40
Adam Powell12190b32010-11-28 19:07:53 -080041/**
42 * A Switch is a two-state toggle switch widget that can select between two
43 * options. The user may drag the "thumb" back and forth to choose the selected option,
44 * or simply tap to toggle as if it were a checkbox.
45 *
46 * @hide
47 */
48public class Switch extends CompoundButton {
49 private static final int TOUCH_MODE_IDLE = 0;
50 private static final int TOUCH_MODE_DOWN = 1;
51 private static final int TOUCH_MODE_DRAGGING = 2;
52
53 // Enum for the "typeface" XML parameter.
54 private static final int SANS = 1;
55 private static final int SERIF = 2;
56 private static final int MONOSPACE = 3;
57
58 private Drawable mThumbDrawable;
59 private Drawable mTrackDrawable;
60 private int mThumbTextPadding;
61 private int mSwitchMinWidth;
62 private int mSwitchPadding;
63 private CharSequence mTextOn;
64 private CharSequence mTextOff;
65
66 private int mTouchMode;
67 private int mTouchSlop;
68 private float mTouchX;
69 private float mTouchY;
70 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
71 private int mMinFlingVelocity;
72
73 private float mThumbPosition;
74 private int mSwitchWidth;
75 private int mSwitchHeight;
76 private int mThumbWidth; // Does not include padding
77
78 private int mSwitchLeft;
79 private int mSwitchTop;
80 private int mSwitchRight;
81 private int mSwitchBottom;
82
83 private TextPaint mTextPaint;
84 private ColorStateList mTextColors;
85 private Layout mOnLayout;
86 private Layout mOffLayout;
87
Adam Powellbe0a4532010-11-29 17:47:48 -080088 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -080089 private final Rect mTempRect = new Rect();
90
91 private static final int[] CHECKED_STATE_SET = {
92 R.attr.state_checked
93 };
94
95 /**
96 * Construct a new Switch with default styling.
97 *
98 * @param context The Context that will determine this widget's theming.
99 */
100 public Switch(Context context) {
101 this(context, null);
102 }
103
104 /**
105 * Construct a new Switch with default styling, overriding specific style
106 * attributes as requested.
107 *
108 * @param context The Context that will determine this widget's theming.
109 * @param attrs Specification of attributes that should deviate from default styling.
110 */
111 public Switch(Context context, AttributeSet attrs) {
112 this(context, attrs, com.android.internal.R.attr.switchStyle);
113 }
114
115 /**
116 * Construct a new Switch with a default style determined by the given theme attribute,
117 * overriding specific style attributes as requested.
118 *
119 * @param context The Context that will determine this widget's theming.
120 * @param attrs Specification of attributes that should deviate from the default styling.
121 * @param defStyle An attribute ID within the active theme containing a reference to the
122 * default style for this widget. e.g. android.R.attr.switchStyle.
123 */
124 public Switch(Context context, AttributeSet attrs, int defStyle) {
125 super(context, attrs, defStyle);
126
127 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
128 Resources res = getResources();
129 mTextPaint.density = res.getDisplayMetrics().density;
130 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
131
132 TypedArray a = context.obtainStyledAttributes(attrs,
133 com.android.internal.R.styleable.Switch, defStyle, 0);
134
135 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_switchThumb);
136 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_switchTrack);
137 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
138 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
139 mThumbTextPadding = a.getDimensionPixelSize(
140 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
141 mSwitchMinWidth = a.getDimensionPixelSize(
142 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
143 mSwitchPadding = a.getDimensionPixelSize(
144 com.android.internal.R.styleable.Switch_switchPadding, 0);
145
146 int appearance = a.getResourceId(
147 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
148 if (appearance != 0) {
149 setSwitchTextAppearance(appearance);
150 }
151 a.recycle();
152
153 ViewConfiguration config = ViewConfiguration.get(context);
154 mTouchSlop = config.getScaledTouchSlop();
155 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
156
157 // Refresh display with current params
158 setChecked(isChecked());
159 }
160
161 /**
162 * Sets the switch text color, size, style, hint color, and highlight color
163 * from the specified TextAppearance resource.
164 */
165 public void setSwitchTextAppearance(int resid) {
166 TypedArray appearance =
167 getContext().obtainStyledAttributes(resid,
168 com.android.internal.R.styleable.TextAppearance);
169
170 ColorStateList colors;
171 int ts;
172
173 colors = appearance.getColorStateList(com.android.internal.R.styleable.
174 TextAppearance_textColor);
175 if (colors != null) {
176 mTextColors = colors;
177 }
178
179 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
180 TextAppearance_textSize, 0);
181 if (ts != 0) {
182 if (ts != mTextPaint.getTextSize()) {
183 mTextPaint.setTextSize(ts);
184 requestLayout();
185 }
186 }
187
188 int typefaceIndex, styleIndex;
189
190 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
191 TextAppearance_typeface, -1);
192 styleIndex = appearance.getInt(com.android.internal.R.styleable.
193 TextAppearance_textStyle, -1);
194
195 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
196
Adam Powell12190b32010-11-28 19:07:53 -0800197 appearance.recycle();
198 }
199
200 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
201 Typeface tf = null;
202 switch (typefaceIndex) {
203 case SANS:
204 tf = Typeface.SANS_SERIF;
205 break;
206
207 case SERIF:
208 tf = Typeface.SERIF;
209 break;
210
211 case MONOSPACE:
212 tf = Typeface.MONOSPACE;
213 break;
214 }
215
216 setSwitchTypeface(tf, styleIndex);
217 }
218
219 /**
220 * Sets the typeface and style in which the text should be displayed on the
221 * switch, and turns on the fake bold and italic bits in the Paint if the
222 * Typeface that you provided does not have all the bits in the
223 * style that you specified.
224 */
225 public void setSwitchTypeface(Typeface tf, int style) {
226 if (style > 0) {
227 if (tf == null) {
228 tf = Typeface.defaultFromStyle(style);
229 } else {
230 tf = Typeface.create(tf, style);
231 }
232
233 setSwitchTypeface(tf);
234 // now compute what (if any) algorithmic styling is needed
235 int typefaceStyle = tf != null ? tf.getStyle() : 0;
236 int need = style & ~typefaceStyle;
237 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
238 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
239 } else {
240 mTextPaint.setFakeBoldText(false);
241 mTextPaint.setTextSkewX(0);
242 setSwitchTypeface(tf);
243 }
244 }
245
246 /**
247 * Sets the typeface and style in which the text should be displayed on the switch.
248 * Note that not all Typeface families actually have bold and italic
249 * variants, so you may need to use
250 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
251 * that you actually want.
252 *
253 * @attr ref android.R.styleable#TextView_typeface
254 * @attr ref android.R.styleable#TextView_textStyle
255 */
256 public void setSwitchTypeface(Typeface tf) {
257 if (mTextPaint.getTypeface() != tf) {
258 mTextPaint.setTypeface(tf);
259
260 requestLayout();
261 invalidate();
262 }
263 }
264
265 /**
266 * Returns the text for when the button is in the checked state.
267 *
268 * @return The text.
269 */
270 public CharSequence getTextOn() {
271 return mTextOn;
272 }
273
274 /**
275 * Sets the text for when the button is in the checked state.
276 *
277 * @param textOn The text.
278 */
279 public void setTextOn(CharSequence textOn) {
280 mTextOn = textOn;
281 requestLayout();
282 }
283
284 /**
285 * Returns the text for when the button is not in the checked state.
286 *
287 * @return The text.
288 */
289 public CharSequence getTextOff() {
290 return mTextOff;
291 }
292
293 /**
294 * Sets the text for when the button is not in the checked state.
295 *
296 * @param textOff The text.
297 */
298 public void setTextOff(CharSequence textOff) {
299 mTextOff = textOff;
300 requestLayout();
301 }
302
303 @Override
304 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
305 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
306 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
307 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
308 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
309
310
311 if (mOnLayout == null) {
312 mOnLayout = makeLayout(mTextOn);
313 }
314 if (mOffLayout == null) {
315 mOffLayout = makeLayout(mTextOff);
316 }
317
318 mTrackDrawable.getPadding(mTempRect);
319 final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth());
320 final int switchWidth = Math.max(mSwitchMinWidth,
321 maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
322 final int switchHeight = mTrackDrawable.getIntrinsicHeight();
323
324 mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
325
326 switch (widthMode) {
327 case MeasureSpec.AT_MOST:
328 widthSize = Math.min(widthSize, switchWidth);
329 break;
330
331 case MeasureSpec.UNSPECIFIED:
332 widthSize = switchWidth;
333 break;
334
335 case MeasureSpec.EXACTLY:
336 // Just use what we were given
337 break;
338 }
339
340 switch (heightMode) {
341 case MeasureSpec.AT_MOST:
342 heightSize = Math.min(heightSize, switchHeight);
343 break;
344
345 case MeasureSpec.UNSPECIFIED:
346 heightSize = switchHeight;
347 break;
348
349 case MeasureSpec.EXACTLY:
350 // Just use what we were given
351 break;
352 }
353
354 mSwitchWidth = switchWidth;
355 mSwitchHeight = switchHeight;
356
357 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Adam Powell12190b32010-11-28 19:07:53 -0800358 final int measuredHeight = getMeasuredHeight();
359 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800360 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800361 }
362 }
363
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700364 @Override
365 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
366 super.onPopulateAccessibilityEvent(event);
367 Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
368 event.getText().add(switchText.getText());
369 }
370
Adam Powell12190b32010-11-28 19:07:53 -0800371 private Layout makeLayout(CharSequence text) {
372 return new StaticLayout(text, mTextPaint,
373 (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)),
374 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
375 }
376
377 /**
378 * @return true if (x, y) is within the target area of the switch thumb
379 */
380 private boolean hitThumb(float x, float y) {
381 mThumbDrawable.getPadding(mTempRect);
382 final int thumbTop = mSwitchTop - mTouchSlop;
383 final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
384 final int thumbRight = thumbLeft + mThumbWidth +
385 mTempRect.left + mTempRect.right + mTouchSlop;
386 final int thumbBottom = mSwitchBottom + mTouchSlop;
387 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
388 }
389
390 @Override
391 public boolean onTouchEvent(MotionEvent ev) {
392 mVelocityTracker.addMovement(ev);
393 final int action = ev.getActionMasked();
394 switch (action) {
395 case MotionEvent.ACTION_DOWN: {
396 final float x = ev.getX();
397 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700398 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800399 mTouchMode = TOUCH_MODE_DOWN;
400 mTouchX = x;
401 mTouchY = y;
402 }
403 break;
404 }
405
406 case MotionEvent.ACTION_MOVE: {
407 switch (mTouchMode) {
408 case TOUCH_MODE_IDLE:
409 // Didn't target the thumb, treat normally.
410 break;
411
412 case TOUCH_MODE_DOWN: {
413 final float x = ev.getX();
414 final float y = ev.getY();
415 if (Math.abs(x - mTouchX) > mTouchSlop ||
416 Math.abs(y - mTouchY) > mTouchSlop) {
417 mTouchMode = TOUCH_MODE_DRAGGING;
418 getParent().requestDisallowInterceptTouchEvent(true);
419 mTouchX = x;
420 mTouchY = y;
421 return true;
422 }
423 break;
424 }
425
426 case TOUCH_MODE_DRAGGING: {
427 final float x = ev.getX();
428 final float dx = x - mTouchX;
429 float newPos = Math.max(0,
430 Math.min(mThumbPosition + dx, getThumbScrollRange()));
431 if (newPos != mThumbPosition) {
432 mThumbPosition = newPos;
433 mTouchX = x;
434 invalidate();
435 }
436 return true;
437 }
438 }
439 break;
440 }
441
442 case MotionEvent.ACTION_UP:
443 case MotionEvent.ACTION_CANCEL: {
444 if (mTouchMode == TOUCH_MODE_DRAGGING) {
445 stopDrag(ev);
446 return true;
447 }
448 mTouchMode = TOUCH_MODE_IDLE;
449 mVelocityTracker.clear();
450 break;
451 }
452 }
453
454 return super.onTouchEvent(ev);
455 }
456
457 private void cancelSuperTouch(MotionEvent ev) {
458 MotionEvent cancel = MotionEvent.obtain(ev);
459 cancel.setAction(MotionEvent.ACTION_CANCEL);
460 super.onTouchEvent(cancel);
461 cancel.recycle();
462 }
463
464 /**
465 * Called from onTouchEvent to end a drag operation.
466 *
467 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
468 */
469 private void stopDrag(MotionEvent ev) {
470 mTouchMode = TOUCH_MODE_IDLE;
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700471 // Up and not canceled, also checks the switch has not been disabled during the drag
472 boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Adam Powell12190b32010-11-28 19:07:53 -0800473
474 cancelSuperTouch(ev);
475
476 if (commitChange) {
477 boolean newState;
478 mVelocityTracker.computeCurrentVelocity(1000);
479 float xvel = mVelocityTracker.getXVelocity();
480 if (Math.abs(xvel) > mMinFlingVelocity) {
481 newState = xvel < 0;
482 } else {
483 newState = getTargetCheckedState();
484 }
485 animateThumbToCheckedState(newState);
486 } else {
487 animateThumbToCheckedState(isChecked());
488 }
489 }
490
491 private void animateThumbToCheckedState(boolean newCheckedState) {
Adam Powell12190b32010-11-28 19:07:53 -0800492 // TODO animate!
Joe Onoratoc3eabb92011-01-07 15:58:44 -0800493 //float targetPos = newCheckedState ? 0 : getThumbScrollRange();
494 //mThumbPosition = targetPos;
Adam Powell12190b32010-11-28 19:07:53 -0800495 setChecked(newCheckedState);
496 }
497
498 private boolean getTargetCheckedState() {
499 return mThumbPosition <= getThumbScrollRange() / 2;
500 }
501
502 @Override
503 public void setChecked(boolean checked) {
504 super.setChecked(checked);
505 mThumbPosition = checked ? 0 : getThumbScrollRange();
Joe Onoratoc3eabb92011-01-07 15:58:44 -0800506 invalidate();
Adam Powell12190b32010-11-28 19:07:53 -0800507 }
508
509 @Override
510 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
511 super.onLayout(changed, left, top, right, bottom);
512
Joe Onoratoc3eabb92011-01-07 15:58:44 -0800513 mThumbPosition = isChecked() ? 0 : getThumbScrollRange();
514
Adam Powell12190b32010-11-28 19:07:53 -0800515 int switchRight = getWidth() - getPaddingRight();
516 int switchLeft = switchRight - mSwitchWidth;
517 int switchTop = 0;
518 int switchBottom = 0;
519 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
520 default:
521 case Gravity.TOP:
522 switchTop = getPaddingTop();
523 switchBottom = switchTop + mSwitchHeight;
524 break;
525
526 case Gravity.CENTER_VERTICAL:
527 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
528 mSwitchHeight / 2;
529 switchBottom = switchTop + mSwitchHeight;
530 break;
531
532 case Gravity.BOTTOM:
533 switchBottom = getHeight() - getPaddingBottom();
534 switchTop = switchBottom - mSwitchHeight;
535 break;
536 }
537
538 mSwitchLeft = switchLeft;
539 mSwitchTop = switchTop;
540 mSwitchBottom = switchBottom;
541 mSwitchRight = switchRight;
542 }
543
544 @Override
545 protected void onDraw(Canvas canvas) {
546 super.onDraw(canvas);
547
548 // Draw the switch
549 int switchLeft = mSwitchLeft;
550 int switchTop = mSwitchTop;
551 int switchRight = mSwitchRight;
552 int switchBottom = mSwitchBottom;
553
554 mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
555 mTrackDrawable.draw(canvas);
556
557 canvas.save();
558
559 mTrackDrawable.getPadding(mTempRect);
560 int switchInnerLeft = switchLeft + mTempRect.left;
561 int switchInnerTop = switchTop + mTempRect.top;
562 int switchInnerRight = switchRight - mTempRect.right;
563 int switchInnerBottom = switchBottom - mTempRect.bottom;
564 canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
565
566 mThumbDrawable.getPadding(mTempRect);
567 final int thumbPos = (int) (mThumbPosition + 0.5f);
568 int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
569 int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
570
571 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
572 mThumbDrawable.draw(canvas);
573
574 mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
575 mTextColors.getDefaultColor()));
576 mTextPaint.drawableState = getDrawableState();
577
578 Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
579
580 canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2,
581 (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
582 switchText.draw(canvas);
583
584 canvas.restore();
585 }
586
587 @Override
588 public int getCompoundPaddingRight() {
589 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
590 if (!TextUtils.isEmpty(getText())) {
591 padding += mSwitchPadding;
592 }
593 return padding;
594 }
595
596 private int getThumbScrollRange() {
597 if (mTrackDrawable == null) {
598 return 0;
599 }
600 mTrackDrawable.getPadding(mTempRect);
601 return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
602 }
603
604 @Override
605 protected int[] onCreateDrawableState(int extraSpace) {
606 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
607 if (isChecked()) {
608 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
609 }
610 return drawableState;
611 }
612
613 @Override
614 protected void drawableStateChanged() {
615 super.drawableStateChanged();
616
617 int[] myDrawableState = getDrawableState();
618
619 // Set the state of the Drawable
620 mThumbDrawable.setState(myDrawableState);
621 mTrackDrawable.setState(myDrawableState);
622
623 invalidate();
624 }
625
626 @Override
627 protected boolean verifyDrawable(Drawable who) {
628 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
629 }
630
631 @Override
632 public void jumpDrawablesToCurrentState() {
633 super.jumpDrawablesToCurrentState();
634 mThumbDrawable.jumpToCurrentState();
635 mTrackDrawable.jumpToCurrentState();
636 }
637}