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