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