Switch.java revision 0c0dde7171c7ea35bbc6d64a41f94b428cb4d1f6
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.animation.ObjectAnimator; 20import android.content.Context; 21import android.content.res.ColorStateList; 22import android.content.res.Resources; 23import android.content.res.TypedArray; 24import android.graphics.Canvas; 25import android.graphics.Insets; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.graphics.Typeface; 29import android.graphics.Region.Op; 30import android.graphics.drawable.Drawable; 31import android.text.Layout; 32import android.text.StaticLayout; 33import android.text.TextPaint; 34import android.text.TextUtils; 35import android.text.method.AllCapsTransformationMethod; 36import android.text.method.TransformationMethod2; 37import android.util.AttributeSet; 38import android.util.FloatProperty; 39import android.util.MathUtils; 40import android.view.Gravity; 41import android.view.MotionEvent; 42import android.view.VelocityTracker; 43import android.view.ViewConfiguration; 44import android.view.accessibility.AccessibilityEvent; 45import android.view.accessibility.AccessibilityNodeInfo; 46 47import com.android.internal.R; 48 49/** 50 * A Switch is a two-state toggle switch widget that can select between two 51 * options. The user may drag the "thumb" back and forth to choose the selected option, 52 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text} 53 * property controls the text displayed in the label for the switch, whereas the 54 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text 55 * controls the text on the thumb. Similarly, the 56 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related 57 * setTypeface() methods control the typeface and style of label text, whereas the 58 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and 59 * the related seSwitchTypeface() methods control that of the thumb. 60 * 61 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a> 62 * guide.</p> 63 * 64 * @attr ref android.R.styleable#Switch_textOn 65 * @attr ref android.R.styleable#Switch_textOff 66 * @attr ref android.R.styleable#Switch_switchMinWidth 67 * @attr ref android.R.styleable#Switch_switchPadding 68 * @attr ref android.R.styleable#Switch_switchTextAppearance 69 * @attr ref android.R.styleable#Switch_thumb 70 * @attr ref android.R.styleable#Switch_thumbTextPadding 71 * @attr ref android.R.styleable#Switch_track 72 */ 73public class Switch extends CompoundButton { 74 private static final int THUMB_ANIMATION_DURATION = 250; 75 76 private static final int TOUCH_MODE_IDLE = 0; 77 private static final int TOUCH_MODE_DOWN = 1; 78 private static final int TOUCH_MODE_DRAGGING = 2; 79 80 // Enum for the "typeface" XML parameter. 81 private static final int SANS = 1; 82 private static final int SERIF = 2; 83 private static final int MONOSPACE = 3; 84 85 private Drawable mThumbDrawable; 86 private Drawable mTrackDrawable; 87 private int mThumbTextPadding; 88 private int mSwitchMinWidth; 89 private int mSwitchPadding; 90 private boolean mSplitTrack; 91 private CharSequence mTextOn; 92 private CharSequence mTextOff; 93 private boolean mShowText; 94 95 private int mTouchMode; 96 private int mTouchSlop; 97 private float mTouchX; 98 private float mTouchY; 99 private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 100 private int mMinFlingVelocity; 101 102 private float mThumbPosition; 103 104 /** 105 * Width required to draw the switch track and thumb. Includes padding and 106 * optical bounds for both the track and thumb. 107 */ 108 private int mSwitchWidth; 109 110 /** 111 * Height required to draw the switch track and thumb. Includes padding and 112 * optical bounds for both the track and thumb. 113 */ 114 private int mSwitchHeight; 115 116 /** 117 * Width of the thumb's content region. Does not include padding or 118 * optical bounds. 119 */ 120 private int mThumbWidth; 121 122 /** Left bound for drawing the switch track and thumb. */ 123 private int mSwitchLeft; 124 125 /** Top bound for drawing the switch track and thumb. */ 126 private int mSwitchTop; 127 128 /** Right bound for drawing the switch track and thumb. */ 129 private int mSwitchRight; 130 131 /** Bottom bound for drawing the switch track and thumb. */ 132 private int mSwitchBottom; 133 134 private TextPaint mTextPaint; 135 private ColorStateList mTextColors; 136 private Layout mOnLayout; 137 private Layout mOffLayout; 138 private TransformationMethod2 mSwitchTransformationMethod; 139 private ObjectAnimator mPositionAnimator; 140 141 @SuppressWarnings("hiding") 142 private final Rect mTempRect = new Rect(); 143 144 private static final int[] CHECKED_STATE_SET = { 145 R.attr.state_checked 146 }; 147 148 /** 149 * Construct a new Switch with default styling. 150 * 151 * @param context The Context that will determine this widget's theming. 152 */ 153 public Switch(Context context) { 154 this(context, null); 155 } 156 157 /** 158 * Construct a new Switch with default styling, overriding specific style 159 * attributes as requested. 160 * 161 * @param context The Context that will determine this widget's theming. 162 * @param attrs Specification of attributes that should deviate from default styling. 163 */ 164 public Switch(Context context, AttributeSet attrs) { 165 this(context, attrs, com.android.internal.R.attr.switchStyle); 166 } 167 168 /** 169 * Construct a new Switch with a default style determined by the given theme attribute, 170 * overriding specific style attributes as requested. 171 * 172 * @param context The Context that will determine this widget's theming. 173 * @param attrs Specification of attributes that should deviate from the default styling. 174 * @param defStyleAttr An attribute in the current theme that contains a 175 * reference to a style resource that supplies default values for 176 * the view. Can be 0 to not look for defaults. 177 */ 178 public Switch(Context context, AttributeSet attrs, int defStyleAttr) { 179 this(context, attrs, defStyleAttr, 0); 180 } 181 182 183 /** 184 * Construct a new Switch with a default style determined by the given theme 185 * attribute or style resource, overriding specific style attributes as 186 * requested. 187 * 188 * @param context The Context that will determine this widget's theming. 189 * @param attrs Specification of attributes that should deviate from the 190 * default styling. 191 * @param defStyleAttr An attribute in the current theme that contains a 192 * reference to a style resource that supplies default values for 193 * the view. Can be 0 to not look for defaults. 194 * @param defStyleRes A resource identifier of a style resource that 195 * supplies default values for the view, used only if 196 * defStyleAttr is 0 or can not be found in the theme. Can be 0 197 * to not look for defaults. 198 */ 199 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 200 super(context, attrs, defStyleAttr, defStyleRes); 201 202 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 203 204 final Resources res = getResources(); 205 mTextPaint.density = res.getDisplayMetrics().density; 206 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); 207 208 final TypedArray a = context.obtainStyledAttributes( 209 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes); 210 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb); 211 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track); 212 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn); 213 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff); 214 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true); 215 mThumbTextPadding = a.getDimensionPixelSize( 216 com.android.internal.R.styleable.Switch_thumbTextPadding, 0); 217 mSwitchMinWidth = a.getDimensionPixelSize( 218 com.android.internal.R.styleable.Switch_switchMinWidth, 0); 219 mSwitchPadding = a.getDimensionPixelSize( 220 com.android.internal.R.styleable.Switch_switchPadding, 0); 221 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false); 222 223 final int appearance = a.getResourceId( 224 com.android.internal.R.styleable.Switch_switchTextAppearance, 0); 225 if (appearance != 0) { 226 setSwitchTextAppearance(context, appearance); 227 } 228 a.recycle(); 229 230 final ViewConfiguration config = ViewConfiguration.get(context); 231 mTouchSlop = config.getScaledTouchSlop(); 232 mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); 233 234 // Refresh display with current params 235 refreshDrawableState(); 236 setChecked(isChecked()); 237 } 238 239 /** 240 * Sets the switch text color, size, style, hint color, and highlight color 241 * from the specified TextAppearance resource. 242 * 243 * @attr ref android.R.styleable#Switch_switchTextAppearance 244 */ 245 public void setSwitchTextAppearance(Context context, int resid) { 246 TypedArray appearance = 247 context.obtainStyledAttributes(resid, 248 com.android.internal.R.styleable.TextAppearance); 249 250 ColorStateList colors; 251 int ts; 252 253 colors = appearance.getColorStateList(com.android.internal.R.styleable. 254 TextAppearance_textColor); 255 if (colors != null) { 256 mTextColors = colors; 257 } else { 258 // If no color set in TextAppearance, default to the view's textColor 259 mTextColors = getTextColors(); 260 } 261 262 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. 263 TextAppearance_textSize, 0); 264 if (ts != 0) { 265 if (ts != mTextPaint.getTextSize()) { 266 mTextPaint.setTextSize(ts); 267 requestLayout(); 268 } 269 } 270 271 int typefaceIndex, styleIndex; 272 273 typefaceIndex = appearance.getInt(com.android.internal.R.styleable. 274 TextAppearance_typeface, -1); 275 styleIndex = appearance.getInt(com.android.internal.R.styleable. 276 TextAppearance_textStyle, -1); 277 278 setSwitchTypefaceByIndex(typefaceIndex, styleIndex); 279 280 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable. 281 TextAppearance_textAllCaps, false); 282 if (allCaps) { 283 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext()); 284 mSwitchTransformationMethod.setLengthChangesAllowed(true); 285 } else { 286 mSwitchTransformationMethod = null; 287 } 288 289 appearance.recycle(); 290 } 291 292 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) { 293 Typeface tf = null; 294 switch (typefaceIndex) { 295 case SANS: 296 tf = Typeface.SANS_SERIF; 297 break; 298 299 case SERIF: 300 tf = Typeface.SERIF; 301 break; 302 303 case MONOSPACE: 304 tf = Typeface.MONOSPACE; 305 break; 306 } 307 308 setSwitchTypeface(tf, styleIndex); 309 } 310 311 /** 312 * Sets the typeface and style in which the text should be displayed on the 313 * switch, and turns on the fake bold and italic bits in the Paint if the 314 * Typeface that you provided does not have all the bits in the 315 * style that you specified. 316 */ 317 public void setSwitchTypeface(Typeface tf, int style) { 318 if (style > 0) { 319 if (tf == null) { 320 tf = Typeface.defaultFromStyle(style); 321 } else { 322 tf = Typeface.create(tf, style); 323 } 324 325 setSwitchTypeface(tf); 326 // now compute what (if any) algorithmic styling is needed 327 int typefaceStyle = tf != null ? tf.getStyle() : 0; 328 int need = style & ~typefaceStyle; 329 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); 330 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); 331 } else { 332 mTextPaint.setFakeBoldText(false); 333 mTextPaint.setTextSkewX(0); 334 setSwitchTypeface(tf); 335 } 336 } 337 338 /** 339 * Sets the typeface in which the text should be displayed on the switch. 340 * Note that not all Typeface families actually have bold and italic 341 * variants, so you may need to use 342 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance 343 * that you actually want. 344 * 345 * @attr ref android.R.styleable#TextView_typeface 346 * @attr ref android.R.styleable#TextView_textStyle 347 */ 348 public void setSwitchTypeface(Typeface tf) { 349 if (mTextPaint.getTypeface() != tf) { 350 mTextPaint.setTypeface(tf); 351 352 requestLayout(); 353 invalidate(); 354 } 355 } 356 357 /** 358 * Set the amount of horizontal padding between the switch and the associated text. 359 * 360 * @param pixels Amount of padding in pixels 361 * 362 * @attr ref android.R.styleable#Switch_switchPadding 363 */ 364 public void setSwitchPadding(int pixels) { 365 mSwitchPadding = pixels; 366 requestLayout(); 367 } 368 369 /** 370 * Get the amount of horizontal padding between the switch and the associated text. 371 * 372 * @return Amount of padding in pixels 373 * 374 * @attr ref android.R.styleable#Switch_switchPadding 375 */ 376 public int getSwitchPadding() { 377 return mSwitchPadding; 378 } 379 380 /** 381 * Set the minimum width of the switch in pixels. The switch's width will be the maximum 382 * of this value and its measured width as determined by the switch drawables and text used. 383 * 384 * @param pixels Minimum width of the switch in pixels 385 * 386 * @attr ref android.R.styleable#Switch_switchMinWidth 387 */ 388 public void setSwitchMinWidth(int pixels) { 389 mSwitchMinWidth = pixels; 390 requestLayout(); 391 } 392 393 /** 394 * Get the minimum width of the switch in pixels. The switch's width will be the maximum 395 * of this value and its measured width as determined by the switch drawables and text used. 396 * 397 * @return Minimum width of the switch in pixels 398 * 399 * @attr ref android.R.styleable#Switch_switchMinWidth 400 */ 401 public int getSwitchMinWidth() { 402 return mSwitchMinWidth; 403 } 404 405 /** 406 * Set the horizontal padding around the text drawn on the switch itself. 407 * 408 * @param pixels Horizontal padding for switch thumb text in pixels 409 * 410 * @attr ref android.R.styleable#Switch_thumbTextPadding 411 */ 412 public void setThumbTextPadding(int pixels) { 413 mThumbTextPadding = pixels; 414 requestLayout(); 415 } 416 417 /** 418 * Get the horizontal padding around the text drawn on the switch itself. 419 * 420 * @return Horizontal padding for switch thumb text in pixels 421 * 422 * @attr ref android.R.styleable#Switch_thumbTextPadding 423 */ 424 public int getThumbTextPadding() { 425 return mThumbTextPadding; 426 } 427 428 /** 429 * Set the drawable used for the track that the switch slides within. 430 * 431 * @param track Track drawable 432 * 433 * @attr ref android.R.styleable#Switch_track 434 */ 435 public void setTrackDrawable(Drawable track) { 436 mTrackDrawable = track; 437 requestLayout(); 438 } 439 440 /** 441 * Set the drawable used for the track that the switch slides within. 442 * 443 * @param resId Resource ID of a track drawable 444 * 445 * @attr ref android.R.styleable#Switch_track 446 */ 447 public void setTrackResource(int resId) { 448 setTrackDrawable(getContext().getDrawable(resId)); 449 } 450 451 /** 452 * Get the drawable used for the track that the switch slides within. 453 * 454 * @return Track drawable 455 * 456 * @attr ref android.R.styleable#Switch_track 457 */ 458 public Drawable getTrackDrawable() { 459 return mTrackDrawable; 460 } 461 462 /** 463 * Set the drawable used for the switch "thumb" - the piece that the user 464 * can physically touch and drag along the track. 465 * 466 * @param thumb Thumb drawable 467 * 468 * @attr ref android.R.styleable#Switch_thumb 469 */ 470 public void setThumbDrawable(Drawable thumb) { 471 mThumbDrawable = thumb; 472 requestLayout(); 473 } 474 475 /** 476 * Set the drawable used for the switch "thumb" - the piece that the user 477 * can physically touch and drag along the track. 478 * 479 * @param resId Resource ID of a thumb drawable 480 * 481 * @attr ref android.R.styleable#Switch_thumb 482 */ 483 public void setThumbResource(int resId) { 484 setThumbDrawable(getContext().getDrawable(resId)); 485 } 486 487 /** 488 * Get the drawable used for the switch "thumb" - the piece that the user 489 * can physically touch and drag along the track. 490 * 491 * @return Thumb drawable 492 * 493 * @attr ref android.R.styleable#Switch_thumb 494 */ 495 public Drawable getThumbDrawable() { 496 return mThumbDrawable; 497 } 498 499 /** 500 * Specifies whether the track should be split by the thumb. When true, 501 * the thumb's optical bounds will be clipped out of the track drawable, 502 * then the thumb will be drawn into the resulting gap. 503 * 504 * @param splitTrack Whether the track should be split by the thumb 505 * 506 * @attr ref android.R.styleable#Switch_splitTrack 507 */ 508 public void setSplitTrack(boolean splitTrack) { 509 mSplitTrack = splitTrack; 510 invalidate(); 511 } 512 513 /** 514 * Returns whether the track should be split by the thumb. 515 * 516 * @attr ref android.R.styleable#Switch_splitTrack 517 */ 518 public boolean getSplitTrack() { 519 return mSplitTrack; 520 } 521 522 /** 523 * Returns the text displayed when the button is in the checked state. 524 * 525 * @attr ref android.R.styleable#Switch_textOn 526 */ 527 public CharSequence getTextOn() { 528 return mTextOn; 529 } 530 531 /** 532 * Sets the text displayed when the button is in the checked state. 533 * 534 * @attr ref android.R.styleable#Switch_textOn 535 */ 536 public void setTextOn(CharSequence textOn) { 537 mTextOn = textOn; 538 requestLayout(); 539 } 540 541 /** 542 * Returns the text displayed when the button is not in the checked state. 543 * 544 * @attr ref android.R.styleable#Switch_textOff 545 */ 546 public CharSequence getTextOff() { 547 return mTextOff; 548 } 549 550 /** 551 * Sets the text displayed when the button is not in the checked state. 552 * 553 * @attr ref android.R.styleable#Switch_textOff 554 */ 555 public void setTextOff(CharSequence textOff) { 556 mTextOff = textOff; 557 requestLayout(); 558 } 559 560 /** 561 * Sets whether the on/off text should be displayed. 562 * 563 * @param showText {@code true} to display on/off text 564 * @attr ref android.R.styleable#Switch_showText 565 */ 566 public void setShowText(boolean showText) { 567 if (mShowText != showText) { 568 mShowText = showText; 569 requestLayout(); 570 } 571 } 572 573 /** 574 * @return whether the on/off text should be displayed 575 * @attr ref android.R.styleable#Switch_showText 576 */ 577 public boolean getShowText() { 578 return mShowText; 579 } 580 581 @Override 582 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 583 if (mShowText) { 584 if (mOnLayout == null) { 585 mOnLayout = makeLayout(mTextOn); 586 } 587 588 if (mOffLayout == null) { 589 mOffLayout = makeLayout(mTextOff); 590 } 591 } 592 593 final Rect padding = mTempRect; 594 final int thumbWidth; 595 final int thumbHeight; 596 if (mThumbDrawable != null) { 597 // Cached thumb width does not include padding. 598 mThumbDrawable.getPadding(padding); 599 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right; 600 thumbHeight = mThumbDrawable.getIntrinsicHeight(); 601 } else { 602 thumbWidth = 0; 603 thumbHeight = 0; 604 } 605 606 final int maxTextWidth; 607 if (mShowText) { 608 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()) 609 + mThumbTextPadding * 2; 610 } else { 611 maxTextWidth = 0; 612 } 613 614 mThumbWidth = Math.max(maxTextWidth, thumbWidth); 615 616 final int trackHeight; 617 if (mTrackDrawable != null) { 618 mTrackDrawable.getPadding(padding); 619 trackHeight = mTrackDrawable.getIntrinsicHeight(); 620 } else { 621 padding.setEmpty(); 622 trackHeight = 0; 623 } 624 625 // Adjust left and right padding to ensure there's enough room for the 626 // thumb's padding (when present). 627 int paddingLeft = padding.left; 628 int paddingRight = padding.right; 629 if (mThumbDrawable != null) { 630 final Insets inset = mThumbDrawable.getOpticalInsets(); 631 paddingLeft = Math.max(paddingLeft, inset.left); 632 paddingRight = Math.max(paddingRight, inset.right); 633 } 634 635 final int switchWidth = Math.max(mSwitchMinWidth, 636 2 * mThumbWidth + paddingLeft + paddingRight); 637 final int switchHeight = Math.max(trackHeight, thumbHeight); 638 mSwitchWidth = switchWidth; 639 mSwitchHeight = switchHeight; 640 641 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 642 643 final int measuredHeight = getMeasuredHeight(); 644 if (measuredHeight < switchHeight) { 645 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight); 646 } 647 } 648 649 @Override 650 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 651 super.onPopulateAccessibilityEvent(event); 652 653 final CharSequence text = isChecked() ? mTextOn : mTextOff; 654 if (text != null) { 655 event.getText().add(text); 656 } 657 } 658 659 private Layout makeLayout(CharSequence text) { 660 final CharSequence transformed = (mSwitchTransformationMethod != null) 661 ? mSwitchTransformationMethod.getTransformation(text, this) 662 : text; 663 664 return new StaticLayout(transformed, mTextPaint, 665 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)), 666 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true); 667 } 668 669 /** 670 * @return true if (x, y) is within the target area of the switch thumb 671 */ 672 private boolean hitThumb(float x, float y) { 673 // Relies on mTempRect, MUST be called first! 674 final int thumbOffset = getThumbOffset(); 675 676 mThumbDrawable.getPadding(mTempRect); 677 final int thumbTop = mSwitchTop - mTouchSlop; 678 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop; 679 final int thumbRight = thumbLeft + mThumbWidth + 680 mTempRect.left + mTempRect.right + mTouchSlop; 681 final int thumbBottom = mSwitchBottom + mTouchSlop; 682 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom; 683 } 684 685 @Override 686 public boolean onTouchEvent(MotionEvent ev) { 687 mVelocityTracker.addMovement(ev); 688 final int action = ev.getActionMasked(); 689 switch (action) { 690 case MotionEvent.ACTION_DOWN: { 691 final float x = ev.getX(); 692 final float y = ev.getY(); 693 if (isEnabled() && hitThumb(x, y)) { 694 mTouchMode = TOUCH_MODE_DOWN; 695 mTouchX = x; 696 mTouchY = y; 697 } 698 break; 699 } 700 701 case MotionEvent.ACTION_MOVE: { 702 switch (mTouchMode) { 703 case TOUCH_MODE_IDLE: 704 // Didn't target the thumb, treat normally. 705 break; 706 707 case TOUCH_MODE_DOWN: { 708 final float x = ev.getX(); 709 final float y = ev.getY(); 710 if (Math.abs(x - mTouchX) > mTouchSlop || 711 Math.abs(y - mTouchY) > mTouchSlop) { 712 mTouchMode = TOUCH_MODE_DRAGGING; 713 getParent().requestDisallowInterceptTouchEvent(true); 714 mTouchX = x; 715 mTouchY = y; 716 return true; 717 } 718 break; 719 } 720 721 case TOUCH_MODE_DRAGGING: { 722 final float x = ev.getX(); 723 final int thumbScrollRange = getThumbScrollRange(); 724 final float thumbScrollOffset = x - mTouchX; 725 float dPos; 726 if (thumbScrollRange != 0) { 727 dPos = thumbScrollOffset / thumbScrollRange; 728 } else { 729 // If the thumb scroll range is empty, just use the 730 // movement direction to snap on or off. 731 dPos = thumbScrollOffset > 0 ? 1 : -1; 732 } 733 if (isLayoutRtl()) { 734 dPos = -dPos; 735 } 736 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1); 737 if (newPos != mThumbPosition) { 738 mTouchX = x; 739 setThumbPosition(newPos); 740 } 741 return true; 742 } 743 } 744 break; 745 } 746 747 case MotionEvent.ACTION_UP: 748 case MotionEvent.ACTION_CANCEL: { 749 if (mTouchMode == TOUCH_MODE_DRAGGING) { 750 stopDrag(ev); 751 // Allow super class to handle pressed state, etc. 752 super.onTouchEvent(ev); 753 return true; 754 } 755 mTouchMode = TOUCH_MODE_IDLE; 756 mVelocityTracker.clear(); 757 break; 758 } 759 } 760 761 return super.onTouchEvent(ev); 762 } 763 764 private void cancelSuperTouch(MotionEvent ev) { 765 MotionEvent cancel = MotionEvent.obtain(ev); 766 cancel.setAction(MotionEvent.ACTION_CANCEL); 767 super.onTouchEvent(cancel); 768 cancel.recycle(); 769 } 770 771 /** 772 * Called from onTouchEvent to end a drag operation. 773 * 774 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL 775 */ 776 private void stopDrag(MotionEvent ev) { 777 mTouchMode = TOUCH_MODE_IDLE; 778 779 // Commit the change if the event is up and not canceled and the switch 780 // has not been disabled during the drag. 781 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); 782 final boolean newState; 783 if (commitChange) { 784 mVelocityTracker.computeCurrentVelocity(1000); 785 final float xvel = mVelocityTracker.getXVelocity(); 786 if (Math.abs(xvel) > mMinFlingVelocity) { 787 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0); 788 } else { 789 newState = getTargetCheckedState(); 790 } 791 } else { 792 newState = isChecked(); 793 } 794 795 setChecked(newState); 796 cancelSuperTouch(ev); 797 } 798 799 private void animateThumbToCheckedState(boolean newCheckedState) { 800 final float targetPosition = newCheckedState ? 1 : 0; 801 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition); 802 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); 803 mPositionAnimator.setAutoCancel(true); 804 mPositionAnimator.start(); 805 } 806 807 private void cancelPositionAnimator() { 808 if (mPositionAnimator != null) { 809 mPositionAnimator.cancel(); 810 } 811 } 812 813 private boolean getTargetCheckedState() { 814 return mThumbPosition > 0.5f; 815 } 816 817 /** 818 * Sets the thumb position as a decimal value between 0 (off) and 1 (on). 819 * 820 * @param position new position between [0,1] 821 */ 822 private void setThumbPosition(float position) { 823 mThumbPosition = position; 824 invalidate(); 825 } 826 827 @Override 828 public void toggle() { 829 setChecked(!isChecked()); 830 } 831 832 @Override 833 public void setChecked(boolean checked) { 834 super.setChecked(checked); 835 836 if (isAttachedToWindow() && isLaidOut()) { 837 animateThumbToCheckedState(checked); 838 } else { 839 // Immediately move the thumb to the new position. 840 cancelPositionAnimator(); 841 setThumbPosition(checked ? 1 : 0); 842 } 843 } 844 845 @Override 846 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 847 super.onLayout(changed, left, top, right, bottom); 848 849 int opticalInsetLeft = 0; 850 int opticalInsetRight = 0; 851 if (mThumbDrawable != null) { 852 final Rect trackPadding = mTempRect; 853 if (mTrackDrawable != null) { 854 mTrackDrawable.getPadding(trackPadding); 855 } else { 856 trackPadding.setEmpty(); 857 } 858 859 final Insets insets = mThumbDrawable.getOpticalInsets(); 860 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left); 861 opticalInsetRight = Math.max(0, insets.right - trackPadding.right); 862 } 863 864 final int switchRight; 865 final int switchLeft; 866 if (isLayoutRtl()) { 867 switchLeft = getPaddingLeft() + opticalInsetLeft; 868 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight; 869 } else { 870 switchRight = getWidth() - getPaddingRight() - opticalInsetRight; 871 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight; 872 } 873 874 final int switchTop; 875 final int switchBottom; 876 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { 877 default: 878 case Gravity.TOP: 879 switchTop = getPaddingTop(); 880 switchBottom = switchTop + mSwitchHeight; 881 break; 882 883 case Gravity.CENTER_VERTICAL: 884 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - 885 mSwitchHeight / 2; 886 switchBottom = switchTop + mSwitchHeight; 887 break; 888 889 case Gravity.BOTTOM: 890 switchBottom = getHeight() - getPaddingBottom(); 891 switchTop = switchBottom - mSwitchHeight; 892 break; 893 } 894 895 mSwitchLeft = switchLeft; 896 mSwitchTop = switchTop; 897 mSwitchBottom = switchBottom; 898 mSwitchRight = switchRight; 899 } 900 901 @Override 902 public void draw(Canvas c) { 903 final Rect padding = mTempRect; 904 final int switchLeft = mSwitchLeft; 905 final int switchTop = mSwitchTop; 906 final int switchRight = mSwitchRight; 907 final int switchBottom = mSwitchBottom; 908 909 int thumbInitialLeft = switchLeft + getThumbOffset(); 910 911 final Insets thumbInsets; 912 if (mThumbDrawable != null) { 913 thumbInsets = mThumbDrawable.getOpticalInsets(); 914 } else { 915 thumbInsets = Insets.NONE; 916 } 917 918 // Layout the track. 919 if (mTrackDrawable != null) { 920 mTrackDrawable.getPadding(padding); 921 922 // Adjust thumb position for track padding. 923 thumbInitialLeft += padding.left; 924 925 // If necessary, offset by the optical insets of the thumb asset. 926 int trackLeft = switchLeft; 927 int trackTop = switchTop; 928 int trackRight = switchRight; 929 int trackBottom = switchBottom; 930 if (thumbInsets != Insets.NONE) { 931 if (thumbInsets.left > padding.left) { 932 trackLeft += thumbInsets.left - padding.left; 933 } 934 if (thumbInsets.top > padding.top) { 935 trackTop += thumbInsets.top - padding.top; 936 } 937 if (thumbInsets.right > padding.right) { 938 trackRight -= thumbInsets.right - padding.right; 939 } 940 if (thumbInsets.bottom > padding.bottom) { 941 trackBottom -= thumbInsets.bottom - padding.bottom; 942 } 943 } 944 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom); 945 } 946 947 // Layout the thumb. 948 if (mThumbDrawable != null) { 949 mThumbDrawable.getPadding(padding); 950 951 final int thumbLeft = thumbInitialLeft - padding.left; 952 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right; 953 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); 954 955 final Drawable background = getBackground(); 956 if (background != null) { 957 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom); 958 } 959 } 960 961 // Draw the background. 962 super.draw(c); 963 } 964 965 @Override 966 protected void onDraw(Canvas canvas) { 967 super.onDraw(canvas); 968 969 final Rect padding = mTempRect; 970 final Drawable trackDrawable = mTrackDrawable; 971 if (trackDrawable != null) { 972 trackDrawable.getPadding(padding); 973 } else { 974 padding.setEmpty(); 975 } 976 977 final int switchTop = mSwitchTop; 978 final int switchBottom = mSwitchBottom; 979 final int switchInnerTop = switchTop + padding.top; 980 final int switchInnerBottom = switchBottom - padding.bottom; 981 982 final Drawable thumbDrawable = mThumbDrawable; 983 if (trackDrawable != null) { 984 if (mSplitTrack && thumbDrawable != null) { 985 final Insets insets = thumbDrawable.getOpticalInsets(); 986 thumbDrawable.copyBounds(padding); 987 padding.left += insets.left; 988 padding.right -= insets.right; 989 990 final int saveCount = canvas.save(); 991 canvas.clipRect(padding, Op.DIFFERENCE); 992 trackDrawable.draw(canvas); 993 canvas.restoreToCount(saveCount); 994 } else { 995 trackDrawable.draw(canvas); 996 } 997 } 998 999 final int saveCount = canvas.save(); 1000 1001 if (thumbDrawable != null) { 1002 thumbDrawable.draw(canvas); 1003 } 1004 1005 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; 1006 if (switchText != null) { 1007 final int drawableState[] = getDrawableState(); 1008 if (mTextColors != null) { 1009 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); 1010 } 1011 mTextPaint.drawableState = drawableState; 1012 1013 final int cX; 1014 if (thumbDrawable != null) { 1015 final Rect bounds = thumbDrawable.getBounds(); 1016 cX = bounds.left + bounds.right; 1017 } else { 1018 cX = getWidth(); 1019 } 1020 1021 final int left = cX / 2 - switchText.getWidth() / 2; 1022 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; 1023 canvas.translate(left, top); 1024 switchText.draw(canvas); 1025 } 1026 1027 canvas.restoreToCount(saveCount); 1028 } 1029 1030 @Override 1031 public int getCompoundPaddingLeft() { 1032 if (!isLayoutRtl()) { 1033 return super.getCompoundPaddingLeft(); 1034 } 1035 int padding = super.getCompoundPaddingLeft() + mSwitchWidth; 1036 if (!TextUtils.isEmpty(getText())) { 1037 padding += mSwitchPadding; 1038 } 1039 return padding; 1040 } 1041 1042 @Override 1043 public int getCompoundPaddingRight() { 1044 if (isLayoutRtl()) { 1045 return super.getCompoundPaddingRight(); 1046 } 1047 int padding = super.getCompoundPaddingRight() + mSwitchWidth; 1048 if (!TextUtils.isEmpty(getText())) { 1049 padding += mSwitchPadding; 1050 } 1051 return padding; 1052 } 1053 1054 /** 1055 * Translates thumb position to offset according to current RTL setting and 1056 * thumb scroll range. Accounts for both track and thumb padding. 1057 * 1058 * @return thumb offset 1059 */ 1060 private int getThumbOffset() { 1061 final float thumbPosition; 1062 if (isLayoutRtl()) { 1063 thumbPosition = 1 - mThumbPosition; 1064 } else { 1065 thumbPosition = mThumbPosition; 1066 } 1067 return (int) (thumbPosition * getThumbScrollRange() + 0.5f); 1068 } 1069 1070 private int getThumbScrollRange() { 1071 if (mTrackDrawable != null) { 1072 final Rect padding = mTempRect; 1073 mTrackDrawable.getPadding(padding); 1074 1075 final Insets insets; 1076 if (mThumbDrawable != null) { 1077 insets = mThumbDrawable.getOpticalInsets(); 1078 } else { 1079 insets = Insets.NONE; 1080 } 1081 1082 return mSwitchWidth - mThumbWidth - padding.left - padding.right 1083 - insets.left - insets.right; 1084 } else { 1085 return 0; 1086 } 1087 } 1088 1089 @Override 1090 protected int[] onCreateDrawableState(int extraSpace) { 1091 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 1092 if (isChecked()) { 1093 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 1094 } 1095 return drawableState; 1096 } 1097 1098 @Override 1099 protected void drawableStateChanged() { 1100 super.drawableStateChanged(); 1101 1102 final int[] myDrawableState = getDrawableState(); 1103 1104 if (mThumbDrawable != null) { 1105 mThumbDrawable.setState(myDrawableState); 1106 } 1107 1108 if (mTrackDrawable != null) { 1109 mTrackDrawable.setState(myDrawableState); 1110 } 1111 1112 invalidate(); 1113 } 1114 1115 @Override 1116 public void drawableHotspotChanged(float x, float y) { 1117 super.drawableHotspotChanged(x, y); 1118 1119 if (mThumbDrawable != null) { 1120 mThumbDrawable.setHotspot(x, y); 1121 } 1122 1123 if (mTrackDrawable != null) { 1124 mTrackDrawable.setHotspot(x, y); 1125 } 1126 } 1127 1128 @Override 1129 protected boolean verifyDrawable(Drawable who) { 1130 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; 1131 } 1132 1133 @Override 1134 public void jumpDrawablesToCurrentState() { 1135 super.jumpDrawablesToCurrentState(); 1136 1137 if (mThumbDrawable != null) { 1138 mThumbDrawable.jumpToCurrentState(); 1139 } 1140 1141 if (mTrackDrawable != null) { 1142 mTrackDrawable.jumpToCurrentState(); 1143 } 1144 1145 if (mPositionAnimator != null && mPositionAnimator.isRunning()) { 1146 mPositionAnimator.end(); 1147 mPositionAnimator = null; 1148 } 1149 } 1150 1151 @Override 1152 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1153 super.onInitializeAccessibilityEvent(event); 1154 event.setClassName(Switch.class.getName()); 1155 } 1156 1157 @Override 1158 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1159 super.onInitializeAccessibilityNodeInfo(info); 1160 info.setClassName(Switch.class.getName()); 1161 CharSequence switchText = isChecked() ? mTextOn : mTextOff; 1162 if (!TextUtils.isEmpty(switchText)) { 1163 CharSequence oldText = info.getText(); 1164 if (TextUtils.isEmpty(oldText)) { 1165 info.setText(switchText); 1166 } else { 1167 StringBuilder newText = new StringBuilder(); 1168 newText.append(oldText).append(' ').append(switchText); 1169 info.setText(newText); 1170 } 1171 } 1172 } 1173 1174 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") { 1175 @Override 1176 public Float get(Switch object) { 1177 return object.mThumbPosition; 1178 } 1179 1180 @Override 1181 public void setValue(Switch object, float value) { 1182 object.setThumbPosition(value); 1183 } 1184 }; 1185} 1186