blob: 57f73ac4f114906a03a3845666d8ddabfd971a89 [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);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700367 if (isChecked()) {
368 CharSequence text = mOnLayout.getText();
369 if (TextUtils.isEmpty(text)) {
370 text = mContext.getString(R.string.switch_on);
371 }
372 event.getText().add(text);
373 } else {
374 CharSequence text = mOffLayout.getText();
375 if (TextUtils.isEmpty(text)) {
376 text = mContext.getString(R.string.switch_off);
377 }
378 event.getText().add(text);
379 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700380 }
381
Adam Powell12190b32010-11-28 19:07:53 -0800382 private Layout makeLayout(CharSequence text) {
383 return new StaticLayout(text, mTextPaint,
384 (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)),
385 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
386 }
387
388 /**
389 * @return true if (x, y) is within the target area of the switch thumb
390 */
391 private boolean hitThumb(float x, float y) {
392 mThumbDrawable.getPadding(mTempRect);
393 final int thumbTop = mSwitchTop - mTouchSlop;
394 final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
395 final int thumbRight = thumbLeft + mThumbWidth +
396 mTempRect.left + mTempRect.right + mTouchSlop;
397 final int thumbBottom = mSwitchBottom + mTouchSlop;
398 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
399 }
400
401 @Override
402 public boolean onTouchEvent(MotionEvent ev) {
403 mVelocityTracker.addMovement(ev);
404 final int action = ev.getActionMasked();
405 switch (action) {
406 case MotionEvent.ACTION_DOWN: {
407 final float x = ev.getX();
408 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700409 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800410 mTouchMode = TOUCH_MODE_DOWN;
411 mTouchX = x;
412 mTouchY = y;
413 }
414 break;
415 }
416
417 case MotionEvent.ACTION_MOVE: {
418 switch (mTouchMode) {
419 case TOUCH_MODE_IDLE:
420 // Didn't target the thumb, treat normally.
421 break;
422
423 case TOUCH_MODE_DOWN: {
424 final float x = ev.getX();
425 final float y = ev.getY();
426 if (Math.abs(x - mTouchX) > mTouchSlop ||
427 Math.abs(y - mTouchY) > mTouchSlop) {
428 mTouchMode = TOUCH_MODE_DRAGGING;
429 getParent().requestDisallowInterceptTouchEvent(true);
430 mTouchX = x;
431 mTouchY = y;
432 return true;
433 }
434 break;
435 }
436
437 case TOUCH_MODE_DRAGGING: {
438 final float x = ev.getX();
439 final float dx = x - mTouchX;
440 float newPos = Math.max(0,
441 Math.min(mThumbPosition + dx, getThumbScrollRange()));
442 if (newPos != mThumbPosition) {
443 mThumbPosition = newPos;
444 mTouchX = x;
445 invalidate();
446 }
447 return true;
448 }
449 }
450 break;
451 }
452
453 case MotionEvent.ACTION_UP:
454 case MotionEvent.ACTION_CANCEL: {
455 if (mTouchMode == TOUCH_MODE_DRAGGING) {
456 stopDrag(ev);
457 return true;
458 }
459 mTouchMode = TOUCH_MODE_IDLE;
460 mVelocityTracker.clear();
461 break;
462 }
463 }
464
465 return super.onTouchEvent(ev);
466 }
467
468 private void cancelSuperTouch(MotionEvent ev) {
469 MotionEvent cancel = MotionEvent.obtain(ev);
470 cancel.setAction(MotionEvent.ACTION_CANCEL);
471 super.onTouchEvent(cancel);
472 cancel.recycle();
473 }
474
475 /**
476 * Called from onTouchEvent to end a drag operation.
477 *
478 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
479 */
480 private void stopDrag(MotionEvent ev) {
481 mTouchMode = TOUCH_MODE_IDLE;
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700482 // Up and not canceled, also checks the switch has not been disabled during the drag
483 boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Adam Powell12190b32010-11-28 19:07:53 -0800484
485 cancelSuperTouch(ev);
486
487 if (commitChange) {
488 boolean newState;
489 mVelocityTracker.computeCurrentVelocity(1000);
490 float xvel = mVelocityTracker.getXVelocity();
491 if (Math.abs(xvel) > mMinFlingVelocity) {
492 newState = xvel < 0;
493 } else {
494 newState = getTargetCheckedState();
495 }
496 animateThumbToCheckedState(newState);
497 } else {
498 animateThumbToCheckedState(isChecked());
499 }
500 }
501
502 private void animateThumbToCheckedState(boolean newCheckedState) {
Adam Powell12190b32010-11-28 19:07:53 -0800503 // TODO animate!
Joe Onoratoc3eabb92011-01-07 15:58:44 -0800504 //float targetPos = newCheckedState ? 0 : getThumbScrollRange();
505 //mThumbPosition = targetPos;
Adam Powell12190b32010-11-28 19:07:53 -0800506 setChecked(newCheckedState);
507 }
508
509 private boolean getTargetCheckedState() {
510 return mThumbPosition <= getThumbScrollRange() / 2;
511 }
512
513 @Override
514 public void setChecked(boolean checked) {
515 super.setChecked(checked);
516 mThumbPosition = checked ? 0 : getThumbScrollRange();
Joe Onoratoc3eabb92011-01-07 15:58:44 -0800517 invalidate();
Adam Powell12190b32010-11-28 19:07:53 -0800518 }
519
520 @Override
521 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
522 super.onLayout(changed, left, top, right, bottom);
523
Joe Onoratoc3eabb92011-01-07 15:58:44 -0800524 mThumbPosition = isChecked() ? 0 : getThumbScrollRange();
525
Adam Powell12190b32010-11-28 19:07:53 -0800526 int switchRight = getWidth() - getPaddingRight();
527 int switchLeft = switchRight - mSwitchWidth;
528 int switchTop = 0;
529 int switchBottom = 0;
530 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
531 default:
532 case Gravity.TOP:
533 switchTop = getPaddingTop();
534 switchBottom = switchTop + mSwitchHeight;
535 break;
536
537 case Gravity.CENTER_VERTICAL:
538 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
539 mSwitchHeight / 2;
540 switchBottom = switchTop + mSwitchHeight;
541 break;
542
543 case Gravity.BOTTOM:
544 switchBottom = getHeight() - getPaddingBottom();
545 switchTop = switchBottom - mSwitchHeight;
546 break;
547 }
548
549 mSwitchLeft = switchLeft;
550 mSwitchTop = switchTop;
551 mSwitchBottom = switchBottom;
552 mSwitchRight = switchRight;
553 }
554
555 @Override
556 protected void onDraw(Canvas canvas) {
557 super.onDraw(canvas);
558
559 // Draw the switch
560 int switchLeft = mSwitchLeft;
561 int switchTop = mSwitchTop;
562 int switchRight = mSwitchRight;
563 int switchBottom = mSwitchBottom;
564
565 mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
566 mTrackDrawable.draw(canvas);
567
568 canvas.save();
569
570 mTrackDrawable.getPadding(mTempRect);
571 int switchInnerLeft = switchLeft + mTempRect.left;
572 int switchInnerTop = switchTop + mTempRect.top;
573 int switchInnerRight = switchRight - mTempRect.right;
574 int switchInnerBottom = switchBottom - mTempRect.bottom;
575 canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
576
577 mThumbDrawable.getPadding(mTempRect);
578 final int thumbPos = (int) (mThumbPosition + 0.5f);
579 int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
580 int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
581
582 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
583 mThumbDrawable.draw(canvas);
584
585 mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
586 mTextColors.getDefaultColor()));
587 mTextPaint.drawableState = getDrawableState();
588
589 Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
590
591 canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2,
592 (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
593 switchText.draw(canvas);
594
595 canvas.restore();
596 }
597
598 @Override
599 public int getCompoundPaddingRight() {
600 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
601 if (!TextUtils.isEmpty(getText())) {
602 padding += mSwitchPadding;
603 }
604 return padding;
605 }
606
607 private int getThumbScrollRange() {
608 if (mTrackDrawable == null) {
609 return 0;
610 }
611 mTrackDrawable.getPadding(mTempRect);
612 return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
613 }
614
615 @Override
616 protected int[] onCreateDrawableState(int extraSpace) {
617 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
618 if (isChecked()) {
619 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
620 }
621 return drawableState;
622 }
623
624 @Override
625 protected void drawableStateChanged() {
626 super.drawableStateChanged();
627
628 int[] myDrawableState = getDrawableState();
629
630 // Set the state of the Drawable
631 mThumbDrawable.setState(myDrawableState);
632 mTrackDrawable.setState(myDrawableState);
633
634 invalidate();
635 }
636
637 @Override
638 protected boolean verifyDrawable(Drawable who) {
639 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
640 }
641
642 @Override
643 public void jumpDrawablesToCurrentState() {
644 super.jumpDrawablesToCurrentState();
645 mThumbDrawable.jumpToCurrentState();
646 mTrackDrawable.jumpToCurrentState();
647 }
648}