Switch.java revision 7650259a597dd24137420d32acc35efc44db381e
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 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 } 380 } 381 382 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(); 409 if (isEnabled() && hitThumb(x, y)) { 410 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; 482 // Up and not canceled, also checks the switch has not been disabled during the drag 483 boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); 484 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) { 503 // TODO animate! 504 //float targetPos = newCheckedState ? 0 : getThumbScrollRange(); 505 //mThumbPosition = targetPos; 506 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(); 517 invalidate(); 518 } 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 524 mThumbPosition = isChecked() ? 0 : getThumbScrollRange(); 525 526 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} 649