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