Switch.java revision 63bce03cc69be4a45230aa8bbd89dbde60681067
1/* 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 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; 37import android.view.accessibility.AccessibilityEvent; 38 39import com.android.internal.R; 40 41/** 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 88 @SuppressWarnings("hiding") 89 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 197 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); 358 final int measuredHeight = getMeasuredHeight(); 359 if (measuredHeight < switchHeight) { 360 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight); 361 } 362 } 363 364 @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 371 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(); 398 if (isEnabled() && hitThumb(x, y)) { 399 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; 471 // Up and not canceled, also checks the switch has not been disabled during the drag 472 boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); 473 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) { 492 // TODO animate! 493 //float targetPos = newCheckedState ? 0 : getThumbScrollRange(); 494 //mThumbPosition = targetPos; 495 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(); 506 invalidate(); 507 } 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 513 mThumbPosition = isChecked() ? 0 : getThumbScrollRange(); 514 515 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} 638