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