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