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