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 17 package android.support.v7.widget; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.Rect; 27 import android.graphics.Region; 28 import android.graphics.Typeface; 29 import android.graphics.drawable.Drawable; 30 import android.os.Build; 31 import android.support.v4.graphics.drawable.DrawableCompat; 32 import android.support.v4.view.MotionEventCompat; 33 import android.support.v4.view.ViewCompat; 34 import android.support.v7.appcompat.R; 35 import android.support.v7.internal.text.AllCapsTransformationMethod; 36 import android.support.v7.internal.widget.DrawableUtils; 37 import android.support.v7.internal.widget.TintManager; 38 import android.support.v7.internal.widget.TintTypedArray; 39 import android.support.v7.internal.widget.ViewUtils; 40 import android.text.Layout; 41 import android.text.StaticLayout; 42 import android.text.TextPaint; 43 import android.text.TextUtils; 44 import android.text.method.TransformationMethod; 45 import android.util.AttributeSet; 46 import android.view.Gravity; 47 import android.view.MotionEvent; 48 import android.view.SoundEffectConstants; 49 import android.view.VelocityTracker; 50 import android.view.ViewConfiguration; 51 import android.view.accessibility.AccessibilityEvent; 52 import android.view.accessibility.AccessibilityNodeInfo; 53 import android.view.animation.Animation; 54 import android.view.animation.Transformation; 55 import 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 */ 73 public 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 */ SwitchCompat(Context context)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 */ SwitchCompat(Context context, AttributeSet attrs)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 */ SwitchCompat(Context context, AttributeSet attrs, int defStyleAttr)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 */ setSwitchTextAppearance(Context context, int resid)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 setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex)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 */ setSwitchTypeface(Typeface tf, int style)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 */ setSwitchTypeface(Typeface tf)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 */ setSwitchPadding(int pixels)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 */ getSwitchPadding()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 */ setSwitchMinWidth(int pixels)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 */ getSwitchMinWidth()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 */ setThumbTextPadding(int pixels)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 */ getThumbTextPadding()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 */ setTrackDrawable(Drawable track)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 */ setTrackResource(int resId)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 */ getTrackDrawable()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 */ setThumbDrawable(Drawable thumb)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 */ setThumbResource(int resId)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 */ getThumbDrawable()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 */ setSplitTrack(boolean splitTrack)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 */ getSplitTrack()469 public boolean getSplitTrack() { 470 return mSplitTrack; 471 } 472 473 /** 474 * Returns the text displayed when the button is in the checked state. 475 */ getTextOn()476 public CharSequence getTextOn() { 477 return mTextOn; 478 } 479 480 /** 481 * Sets the text displayed when the button is in the checked state. 482 */ setTextOn(CharSequence textOn)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 */ getTextOff()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 */ setTextOff(CharSequence textOff)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 */ setShowText(boolean showText)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 */ getShowText()518 public boolean getShowText() { 519 return mShowText; 520 } 521 522 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 onPopulateAccessibilityEvent(AccessibilityEvent event)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 makeLayout(CharSequence text)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 */ hitThumb(float x, float y)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 onTouchEvent(MotionEvent ev)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 cancelSuperTouch(MotionEvent ev)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 */ stopDrag(MotionEvent ev)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 setChecked(newState); 746 } 747 cancelSuperTouch(ev); 748 } 749 animateThumbToCheckedState(boolean newCheckedState)750 private void animateThumbToCheckedState(boolean newCheckedState) { 751 mPositionAnimator = new ThumbAnimation(mThumbPosition, newCheckedState ? 1 : 0); 752 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); 753 startAnimation(mPositionAnimator); 754 } 755 cancelPositionAnimator()756 private void cancelPositionAnimator() { 757 if (mPositionAnimator != null) { 758 clearAnimation(); 759 mPositionAnimator = null; 760 } 761 } 762 getTargetCheckedState()763 private boolean getTargetCheckedState() { 764 return mThumbPosition > 0.5f; 765 } 766 767 /** 768 * Sets the thumb position as a decimal value between 0 (off) and 1 (on). 769 * 770 * @param position new position between [0,1] 771 */ setThumbPosition(float position)772 private void setThumbPosition(float position) { 773 mThumbPosition = position; 774 invalidate(); 775 } 776 777 @Override toggle()778 public void toggle() { 779 setChecked(!isChecked()); 780 } 781 782 @Override setChecked(boolean checked)783 public void setChecked(boolean checked) { 784 super.setChecked(checked); 785 786 // Calling the super method may result in setChecked() getting called 787 // recursively with a different value, so load the REAL value... 788 checked = isChecked(); 789 790 if (getWindowToken() != null && ViewCompat.isLaidOut(this)) { 791 animateThumbToCheckedState(checked); 792 } else { 793 // Immediately move the thumb to the new position. 794 cancelPositionAnimator(); 795 setThumbPosition(checked ? 1 : 0); 796 } 797 } 798 799 @Override onLayout(boolean changed, int left, int top, int right, int bottom)800 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 801 super.onLayout(changed, left, top, right, bottom); 802 803 int opticalInsetLeft = 0; 804 int opticalInsetRight = 0; 805 if (mThumbDrawable != null) { 806 final Rect trackPadding = mTempRect; 807 if (mTrackDrawable != null) { 808 mTrackDrawable.getPadding(trackPadding); 809 } else { 810 trackPadding.setEmpty(); 811 } 812 813 final Rect insets = DrawableUtils.getOpticalBounds(mThumbDrawable); 814 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left); 815 opticalInsetRight = Math.max(0, insets.right - trackPadding.right); 816 } 817 818 final int switchRight; 819 final int switchLeft; 820 if (ViewUtils.isLayoutRtl(this)) { 821 switchLeft = getPaddingLeft() + opticalInsetLeft; 822 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight; 823 } else { 824 switchRight = getWidth() - getPaddingRight() - opticalInsetRight; 825 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight; 826 } 827 828 final int switchTop; 829 final int switchBottom; 830 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { 831 default: 832 case Gravity.TOP: 833 switchTop = getPaddingTop(); 834 switchBottom = switchTop + mSwitchHeight; 835 break; 836 837 case Gravity.CENTER_VERTICAL: 838 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - 839 mSwitchHeight / 2; 840 switchBottom = switchTop + mSwitchHeight; 841 break; 842 843 case Gravity.BOTTOM: 844 switchBottom = getHeight() - getPaddingBottom(); 845 switchTop = switchBottom - mSwitchHeight; 846 break; 847 } 848 849 mSwitchLeft = switchLeft; 850 mSwitchTop = switchTop; 851 mSwitchBottom = switchBottom; 852 mSwitchRight = switchRight; 853 } 854 855 @Override draw(Canvas c)856 public void draw(Canvas c) { 857 final Rect padding = mTempRect; 858 final int switchLeft = mSwitchLeft; 859 final int switchTop = mSwitchTop; 860 final int switchRight = mSwitchRight; 861 final int switchBottom = mSwitchBottom; 862 863 int thumbInitialLeft = switchLeft + getThumbOffset(); 864 865 final Rect thumbInsets; 866 if (mThumbDrawable != null) { 867 thumbInsets = DrawableUtils.getOpticalBounds(mThumbDrawable); 868 } else { 869 thumbInsets = DrawableUtils.INSETS_NONE; 870 } 871 872 // Layout the track. 873 if (mTrackDrawable != null) { 874 mTrackDrawable.getPadding(padding); 875 876 // Adjust thumb position for track padding. 877 thumbInitialLeft += padding.left; 878 879 // If necessary, offset by the optical insets of the thumb asset. 880 int trackLeft = switchLeft; 881 int trackTop = switchTop; 882 int trackRight = switchRight; 883 int trackBottom = switchBottom; 884 if (thumbInsets != null && !thumbInsets.isEmpty()) { 885 if (thumbInsets.left > padding.left) { 886 trackLeft += thumbInsets.left - padding.left; 887 } 888 if (thumbInsets.top > padding.top) { 889 trackTop += thumbInsets.top - padding.top; 890 } 891 if (thumbInsets.right > padding.right) { 892 trackRight -= thumbInsets.right - padding.right; 893 } 894 if (thumbInsets.bottom > padding.bottom) { 895 trackBottom -= thumbInsets.bottom - padding.bottom; 896 } 897 } 898 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom); 899 } 900 901 // Layout the thumb. 902 if (mThumbDrawable != null) { 903 mThumbDrawable.getPadding(padding); 904 905 final int thumbLeft = thumbInitialLeft - padding.left; 906 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right; 907 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); 908 909 final Drawable background = getBackground(); 910 if (background != null) { 911 DrawableCompat.setHotspotBounds(background, thumbLeft, switchTop, 912 thumbRight, switchBottom); 913 } 914 } 915 916 // Draw the background. 917 super.draw(c); 918 } 919 920 @Override onDraw(Canvas canvas)921 protected void onDraw(Canvas canvas) { 922 super.onDraw(canvas); 923 924 final Rect padding = mTempRect; 925 final Drawable trackDrawable = mTrackDrawable; 926 if (trackDrawable != null) { 927 trackDrawable.getPadding(padding); 928 } else { 929 padding.setEmpty(); 930 } 931 932 final int switchTop = mSwitchTop; 933 final int switchBottom = mSwitchBottom; 934 final int switchInnerTop = switchTop + padding.top; 935 final int switchInnerBottom = switchBottom - padding.bottom; 936 937 final Drawable thumbDrawable = mThumbDrawable; 938 if (trackDrawable != null) { 939 if (mSplitTrack && thumbDrawable != null) { 940 final Rect insets = DrawableUtils.getOpticalBounds(thumbDrawable); 941 thumbDrawable.copyBounds(padding); 942 padding.left += insets.left; 943 padding.right -= insets.right; 944 945 final int saveCount = canvas.save(); 946 canvas.clipRect(padding, Region.Op.DIFFERENCE); 947 trackDrawable.draw(canvas); 948 canvas.restoreToCount(saveCount); 949 } else { 950 trackDrawable.draw(canvas); 951 } 952 } 953 954 final int saveCount = canvas.save(); 955 956 if (thumbDrawable != null) { 957 thumbDrawable.draw(canvas); 958 } 959 960 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; 961 if (switchText != null) { 962 final int drawableState[] = getDrawableState(); 963 if (mTextColors != null) { 964 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); 965 } 966 mTextPaint.drawableState = drawableState; 967 968 final int cX; 969 if (thumbDrawable != null) { 970 final Rect bounds = thumbDrawable.getBounds(); 971 cX = bounds.left + bounds.right; 972 } else { 973 cX = getWidth(); 974 } 975 976 final int left = cX / 2 - switchText.getWidth() / 2; 977 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; 978 canvas.translate(left, top); 979 switchText.draw(canvas); 980 } 981 982 canvas.restoreToCount(saveCount); 983 } 984 985 @Override getCompoundPaddingLeft()986 public int getCompoundPaddingLeft() { 987 if (!ViewUtils.isLayoutRtl(this)) { 988 return super.getCompoundPaddingLeft(); 989 } 990 int padding = super.getCompoundPaddingLeft() + mSwitchWidth; 991 if (!TextUtils.isEmpty(getText())) { 992 padding += mSwitchPadding; 993 } 994 return padding; 995 } 996 997 @Override getCompoundPaddingRight()998 public int getCompoundPaddingRight() { 999 if (ViewUtils.isLayoutRtl(this)) { 1000 return super.getCompoundPaddingRight(); 1001 } 1002 int padding = super.getCompoundPaddingRight() + mSwitchWidth; 1003 if (!TextUtils.isEmpty(getText())) { 1004 padding += mSwitchPadding; 1005 } 1006 return padding; 1007 } 1008 1009 /** 1010 * Translates thumb position to offset according to current RTL setting and 1011 * thumb scroll range. Accounts for both track and thumb padding. 1012 * 1013 * @return thumb offset 1014 */ getThumbOffset()1015 private int getThumbOffset() { 1016 final float thumbPosition; 1017 if (ViewUtils.isLayoutRtl(this)) { 1018 thumbPosition = 1 - mThumbPosition; 1019 } else { 1020 thumbPosition = mThumbPosition; 1021 } 1022 return (int) (thumbPosition * getThumbScrollRange() + 0.5f); 1023 } 1024 getThumbScrollRange()1025 private int getThumbScrollRange() { 1026 if (mTrackDrawable != null) { 1027 final Rect padding = mTempRect; 1028 mTrackDrawable.getPadding(padding); 1029 1030 final Rect insets; 1031 if (mThumbDrawable != null) { 1032 insets = DrawableUtils.getOpticalBounds(mThumbDrawable); 1033 } else { 1034 insets = DrawableUtils.INSETS_NONE; 1035 } 1036 1037 return mSwitchWidth - mThumbWidth - padding.left - padding.right 1038 - insets.left - insets.right; 1039 } else { 1040 return 0; 1041 } 1042 } 1043 1044 @Override onCreateDrawableState(int extraSpace)1045 protected int[] onCreateDrawableState(int extraSpace) { 1046 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 1047 if (isChecked()) { 1048 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 1049 } 1050 return drawableState; 1051 } 1052 1053 @Override drawableStateChanged()1054 protected void drawableStateChanged() { 1055 super.drawableStateChanged(); 1056 1057 final int[] myDrawableState = getDrawableState(); 1058 1059 if (mThumbDrawable != null) { 1060 mThumbDrawable.setState(myDrawableState); 1061 } 1062 1063 if (mTrackDrawable != null) { 1064 mTrackDrawable.setState(myDrawableState); 1065 } 1066 1067 invalidate(); 1068 } 1069 1070 @Override drawableHotspotChanged(float x, float y)1071 public void drawableHotspotChanged(float x, float y) { 1072 if (Build.VERSION.SDK_INT >= 21) { 1073 super.drawableHotspotChanged(x, y); 1074 } 1075 1076 if (mThumbDrawable != null) { 1077 DrawableCompat.setHotspot(mThumbDrawable, x, y); 1078 } 1079 1080 if (mTrackDrawable != null) { 1081 DrawableCompat.setHotspot(mTrackDrawable, x, y); 1082 } 1083 } 1084 1085 @Override verifyDrawable(Drawable who)1086 protected boolean verifyDrawable(Drawable who) { 1087 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; 1088 } 1089 1090 @Override jumpDrawablesToCurrentState()1091 public void jumpDrawablesToCurrentState() { 1092 if (Build.VERSION.SDK_INT >= 11) { 1093 super.jumpDrawablesToCurrentState(); 1094 1095 if (mThumbDrawable != null) { 1096 mThumbDrawable.jumpToCurrentState(); 1097 } 1098 1099 if (mTrackDrawable != null) { 1100 mTrackDrawable.jumpToCurrentState(); 1101 } 1102 1103 if (mPositionAnimator != null && !mPositionAnimator.hasEnded()) { 1104 clearAnimation(); 1105 // Manually set our thumb position to the end state 1106 setThumbPosition(mPositionAnimator.mEndPosition); 1107 mPositionAnimator = null; 1108 } 1109 } 1110 } 1111 1112 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1113 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1114 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1115 super.onInitializeAccessibilityEvent(event); 1116 event.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME); 1117 } 1118 1119 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1120 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1121 if (Build.VERSION.SDK_INT >= 14) { 1122 super.onInitializeAccessibilityNodeInfo(info); 1123 info.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME); 1124 CharSequence switchText = isChecked() ? mTextOn : mTextOff; 1125 if (!TextUtils.isEmpty(switchText)) { 1126 CharSequence oldText = info.getText(); 1127 if (TextUtils.isEmpty(oldText)) { 1128 info.setText(switchText); 1129 } else { 1130 StringBuilder newText = new StringBuilder(); 1131 newText.append(oldText).append(' ').append(switchText); 1132 info.setText(newText); 1133 } 1134 } 1135 } 1136 } 1137 1138 /** 1139 * Taken from android.util.MathUtils 1140 */ constrain(float amount, float low, float high)1141 private static float constrain(float amount, float low, float high) { 1142 return amount < low ? low : (amount > high ? high : amount); 1143 } 1144 1145 private class ThumbAnimation extends Animation { 1146 final float mStartPosition; 1147 final float mEndPosition; 1148 final float mDiff; 1149 ThumbAnimation(float startPosition, float endPosition)1150 private ThumbAnimation(float startPosition, float endPosition) { 1151 mStartPosition = startPosition; 1152 mEndPosition = endPosition; 1153 mDiff = endPosition - startPosition; 1154 } 1155 1156 @Override applyTransformation(float interpolatedTime, Transformation t)1157 protected void applyTransformation(float interpolatedTime, Transformation t) { 1158 setThumbPosition(mStartPosition + (mDiff * interpolatedTime)); 1159 } 1160 } 1161 }