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.PorterDuff; 27 import android.graphics.Rect; 28 import android.graphics.Region; 29 import android.graphics.Typeface; 30 import android.graphics.drawable.Drawable; 31 import android.os.Build; 32 import android.support.annotation.Nullable; 33 import android.support.v4.graphics.drawable.DrawableCompat; 34 import android.support.v4.view.MotionEventCompat; 35 import android.support.v4.view.ViewCompat; 36 import android.support.v7.appcompat.R; 37 import android.support.v7.text.AllCapsTransformationMethod; 38 import android.text.Layout; 39 import android.text.StaticLayout; 40 import android.text.TextPaint; 41 import android.text.TextUtils; 42 import android.text.method.TransformationMethod; 43 import android.util.AttributeSet; 44 import android.view.Gravity; 45 import android.view.MotionEvent; 46 import android.view.SoundEffectConstants; 47 import android.view.VelocityTracker; 48 import android.view.ViewConfiguration; 49 import android.view.accessibility.AccessibilityEvent; 50 import android.view.accessibility.AccessibilityNodeInfo; 51 import android.view.animation.Animation; 52 import android.view.animation.Transformation; 53 import 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 setSwitchTypeface() methods control that of the thumb. 70 * 71 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a> 72 * guide.</p> 73 * 74 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_textOn 75 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_textOff 76 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchMinWidth 77 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchPadding 78 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchTextAppearance 79 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_thumb 80 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTextPadding 81 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_track 82 */ 83 public class SwitchCompat extends CompoundButton { 84 private static final int THUMB_ANIMATION_DURATION = 250; 85 86 private static final int TOUCH_MODE_IDLE = 0; 87 private static final int TOUCH_MODE_DOWN = 1; 88 private static final int TOUCH_MODE_DRAGGING = 2; 89 90 // We force the accessibility events to have a class name of Switch, since screen readers 91 // already know how to handle their events 92 private static final String ACCESSIBILITY_EVENT_CLASS_NAME = "android.widget.Switch"; 93 94 // Enum for the "typeface" XML parameter. 95 private static final int SANS = 1; 96 private static final int SERIF = 2; 97 private static final int MONOSPACE = 3; 98 99 private Drawable mThumbDrawable; 100 private ColorStateList mThumbTintList = null; 101 private PorterDuff.Mode mThumbTintMode = null; 102 private boolean mHasThumbTint = false; 103 private boolean mHasThumbTintMode = false; 104 105 private Drawable mTrackDrawable; 106 private ColorStateList mTrackTintList = null; 107 private PorterDuff.Mode mTrackTintMode = null; 108 private boolean mHasTrackTint = false; 109 private boolean mHasTrackTintMode = false; 110 111 private int mThumbTextPadding; 112 private int mSwitchMinWidth; 113 private int mSwitchPadding; 114 private boolean mSplitTrack; 115 private CharSequence mTextOn; 116 private CharSequence mTextOff; 117 private boolean mShowText; 118 119 private int mTouchMode; 120 private int mTouchSlop; 121 private float mTouchX; 122 private float mTouchY; 123 private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 124 private int mMinFlingVelocity; 125 126 private float mThumbPosition; 127 128 /** 129 * Width required to draw the switch track and thumb. Includes padding and 130 * optical bounds for both the track and thumb. 131 */ 132 private int mSwitchWidth; 133 134 /** 135 * Height required to draw the switch track and thumb. Includes padding and 136 * optical bounds for both the track and thumb. 137 */ 138 private int mSwitchHeight; 139 140 /** 141 * Width of the thumb's content region. Does not include padding or 142 * optical bounds. 143 */ 144 private int mThumbWidth; 145 146 /** Left bound for drawing the switch track and thumb. */ 147 private int mSwitchLeft; 148 149 /** Top bound for drawing the switch track and thumb. */ 150 private int mSwitchTop; 151 152 /** Right bound for drawing the switch track and thumb. */ 153 private int mSwitchRight; 154 155 /** Bottom bound for drawing the switch track and thumb. */ 156 private int mSwitchBottom; 157 158 private TextPaint mTextPaint; 159 private ColorStateList mTextColors; 160 private Layout mOnLayout; 161 private Layout mOffLayout; 162 private TransformationMethod mSwitchTransformationMethod; 163 private ThumbAnimation mPositionAnimator; 164 165 @SuppressWarnings("hiding") 166 private final Rect mTempRect = new Rect(); 167 168 private final AppCompatDrawableManager mDrawableManager; 169 170 private static final int[] CHECKED_STATE_SET = { 171 android.R.attr.state_checked 172 }; 173 174 /** 175 * Construct a new Switch with default styling. 176 * 177 * @param context The Context that will determine this widget's theming. 178 */ SwitchCompat(Context context)179 public SwitchCompat(Context context) { 180 this(context, null); 181 } 182 183 /** 184 * Construct a new Switch with default styling, overriding specific style 185 * attributes as requested. 186 * 187 * @param context The Context that will determine this widget's theming. 188 * @param attrs Specification of attributes that should deviate from default styling. 189 */ SwitchCompat(Context context, AttributeSet attrs)190 public SwitchCompat(Context context, AttributeSet attrs) { 191 this(context, attrs, R.attr.switchStyle); 192 } 193 194 /** 195 * Construct a new Switch with a default style determined by the given theme attribute, 196 * overriding specific style attributes as requested. 197 * 198 * @param context The Context that will determine this widget's theming. 199 * @param attrs Specification of attributes that should deviate from the default styling. 200 * @param defStyleAttr An attribute in the current theme that contains a 201 * reference to a style resource that supplies default values for 202 * the view. Can be 0 to not look for defaults. 203 */ SwitchCompat(Context context, AttributeSet attrs, int defStyleAttr)204 public SwitchCompat(Context context, AttributeSet attrs, int defStyleAttr) { 205 super(context, attrs, defStyleAttr); 206 207 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 208 209 final Resources res = getResources(); 210 mTextPaint.density = res.getDisplayMetrics().density; 211 212 final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, 213 attrs, R.styleable.SwitchCompat, defStyleAttr, 0); 214 mThumbDrawable = a.getDrawable(R.styleable.SwitchCompat_android_thumb); 215 if (mThumbDrawable != null) { 216 mThumbDrawable.setCallback(this); 217 } 218 mTrackDrawable = a.getDrawable(R.styleable.SwitchCompat_track); 219 if (mTrackDrawable != null) { 220 mTrackDrawable.setCallback(this); 221 } 222 mTextOn = a.getText(R.styleable.SwitchCompat_android_textOn); 223 mTextOff = a.getText(R.styleable.SwitchCompat_android_textOff); 224 mShowText = a.getBoolean(R.styleable.SwitchCompat_showText, true); 225 mThumbTextPadding = a.getDimensionPixelSize( 226 R.styleable.SwitchCompat_thumbTextPadding, 0); 227 mSwitchMinWidth = a.getDimensionPixelSize( 228 R.styleable.SwitchCompat_switchMinWidth, 0); 229 mSwitchPadding = a.getDimensionPixelSize( 230 R.styleable.SwitchCompat_switchPadding, 0); 231 mSplitTrack = a.getBoolean(R.styleable.SwitchCompat_splitTrack, false); 232 233 ColorStateList thumbTintList = a.getColorStateList(R.styleable.SwitchCompat_thumbTint); 234 if (thumbTintList != null) { 235 mThumbTintList = thumbTintList; 236 mHasThumbTint = true; 237 } 238 PorterDuff.Mode thumbTintMode = DrawableUtils.parseTintMode( 239 a.getInt(R.styleable.SwitchCompat_thumbTintMode, -1), null); 240 if (mThumbTintMode != thumbTintMode) { 241 mThumbTintMode = thumbTintMode; 242 mHasThumbTintMode = true; 243 } 244 if (mHasThumbTint || mHasThumbTintMode) { 245 applyThumbTint(); 246 } 247 248 ColorStateList trackTintList = a.getColorStateList(R.styleable.SwitchCompat_trackTint); 249 if (trackTintList != null) { 250 mTrackTintList = trackTintList; 251 mHasTrackTint = true; 252 } 253 PorterDuff.Mode trackTintMode = DrawableUtils.parseTintMode( 254 a.getInt(R.styleable.SwitchCompat_trackTintMode, -1), null); 255 if (mTrackTintMode != trackTintMode) { 256 mTrackTintMode = trackTintMode; 257 mHasTrackTintMode = true; 258 } 259 if (mHasTrackTint || mHasTrackTintMode) { 260 applyTrackTint(); 261 } 262 263 final int appearance = a.getResourceId( 264 R.styleable.SwitchCompat_switchTextAppearance, 0); 265 if (appearance != 0) { 266 setSwitchTextAppearance(context, appearance); 267 } 268 269 mDrawableManager = AppCompatDrawableManager.get(); 270 271 a.recycle(); 272 273 final ViewConfiguration config = ViewConfiguration.get(context); 274 mTouchSlop = config.getScaledTouchSlop(); 275 mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); 276 277 // Refresh display with current params 278 refreshDrawableState(); 279 setChecked(isChecked()); 280 } 281 282 /** 283 * Sets the switch text color, size, style, hint color, and highlight color 284 * from the specified TextAppearance resource. 285 * 286 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchTextAppearance 287 */ setSwitchTextAppearance(Context context, int resid)288 public void setSwitchTextAppearance(Context context, int resid) { 289 TypedArray appearance = context.obtainStyledAttributes(resid, R.styleable.TextAppearance); 290 291 ColorStateList colors; 292 int ts; 293 294 colors = appearance.getColorStateList(R.styleable.TextAppearance_android_textColor); 295 if (colors != null) { 296 mTextColors = colors; 297 } else { 298 // If no color set in TextAppearance, default to the view's textColor 299 mTextColors = getTextColors(); 300 } 301 302 ts = appearance.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0); 303 if (ts != 0) { 304 if (ts != mTextPaint.getTextSize()) { 305 mTextPaint.setTextSize(ts); 306 requestLayout(); 307 } 308 } 309 310 int typefaceIndex, styleIndex; 311 typefaceIndex = appearance.getInt(R.styleable.TextAppearance_android_typeface, -1); 312 styleIndex = appearance.getInt(R.styleable.TextAppearance_android_textStyle, -1); 313 314 setSwitchTypefaceByIndex(typefaceIndex, styleIndex); 315 316 boolean allCaps = appearance.getBoolean(R.styleable.TextAppearance_textAllCaps, false); 317 if (allCaps) { 318 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext()); 319 } else { 320 mSwitchTransformationMethod = null; 321 } 322 323 appearance.recycle(); 324 } 325 setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex)326 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) { 327 Typeface tf = null; 328 switch (typefaceIndex) { 329 case SANS: 330 tf = Typeface.SANS_SERIF; 331 break; 332 333 case SERIF: 334 tf = Typeface.SERIF; 335 break; 336 337 case MONOSPACE: 338 tf = Typeface.MONOSPACE; 339 break; 340 } 341 342 setSwitchTypeface(tf, styleIndex); 343 } 344 345 /** 346 * Sets the typeface and style in which the text should be displayed on the 347 * switch, and turns on the fake bold and italic bits in the Paint if the 348 * Typeface that you provided does not have all the bits in the 349 * style that you specified. 350 */ setSwitchTypeface(Typeface tf, int style)351 public void setSwitchTypeface(Typeface tf, int style) { 352 if (style > 0) { 353 if (tf == null) { 354 tf = Typeface.defaultFromStyle(style); 355 } else { 356 tf = Typeface.create(tf, style); 357 } 358 359 setSwitchTypeface(tf); 360 // now compute what (if any) algorithmic styling is needed 361 int typefaceStyle = tf != null ? tf.getStyle() : 0; 362 int need = style & ~typefaceStyle; 363 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); 364 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); 365 } else { 366 mTextPaint.setFakeBoldText(false); 367 mTextPaint.setTextSkewX(0); 368 setSwitchTypeface(tf); 369 } 370 } 371 372 /** 373 * Sets the typeface in which the text should be displayed on the switch. 374 * Note that not all Typeface families actually have bold and italic 375 * variants, so you may need to use 376 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance 377 * that you actually want. 378 */ setSwitchTypeface(Typeface tf)379 public void setSwitchTypeface(Typeface tf) { 380 if (mTextPaint.getTypeface() != tf) { 381 mTextPaint.setTypeface(tf); 382 383 requestLayout(); 384 invalidate(); 385 } 386 } 387 388 /** 389 * Set the amount of horizontal padding between the switch and the associated text. 390 * 391 * @param pixels Amount of padding in pixels 392 * 393 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchPadding 394 */ setSwitchPadding(int pixels)395 public void setSwitchPadding(int pixels) { 396 mSwitchPadding = pixels; 397 requestLayout(); 398 } 399 400 /** 401 * Get the amount of horizontal padding between the switch and the associated text. 402 * 403 * @return Amount of padding in pixels 404 * 405 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchPadding 406 */ getSwitchPadding()407 public int getSwitchPadding() { 408 return mSwitchPadding; 409 } 410 411 /** 412 * Set the minimum width of the switch in pixels. The switch's width will be the maximum 413 * of this value and its measured width as determined by the switch drawables and text used. 414 * 415 * @param pixels Minimum width of the switch in pixels 416 * 417 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchMinWidth 418 */ setSwitchMinWidth(int pixels)419 public void setSwitchMinWidth(int pixels) { 420 mSwitchMinWidth = pixels; 421 requestLayout(); 422 } 423 424 /** 425 * Get the minimum width of the switch in pixels. The switch's width will be the maximum 426 * of this value and its measured width as determined by the switch drawables and text used. 427 * 428 * @return Minimum width of the switch in pixels 429 * 430 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_switchMinWidth 431 */ getSwitchMinWidth()432 public int getSwitchMinWidth() { 433 return mSwitchMinWidth; 434 } 435 436 /** 437 * Set the horizontal padding around the text drawn on the switch itself. 438 * 439 * @param pixels Horizontal padding for switch thumb text in pixels 440 * 441 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTextPadding 442 */ setThumbTextPadding(int pixels)443 public void setThumbTextPadding(int pixels) { 444 mThumbTextPadding = pixels; 445 requestLayout(); 446 } 447 448 /** 449 * Get the horizontal padding around the text drawn on the switch itself. 450 * 451 * @return Horizontal padding for switch thumb text in pixels 452 * 453 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTextPadding 454 */ getThumbTextPadding()455 public int getThumbTextPadding() { 456 return mThumbTextPadding; 457 } 458 459 /** 460 * Set the drawable used for the track that the switch slides within. 461 * 462 * @param track Track drawable 463 * 464 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_track 465 */ setTrackDrawable(Drawable track)466 public void setTrackDrawable(Drawable track) { 467 if (mTrackDrawable != null) { 468 mTrackDrawable.setCallback(null); 469 } 470 mTrackDrawable = track; 471 if (track != null) { 472 track.setCallback(this); 473 } 474 requestLayout(); 475 } 476 477 /** 478 * Set the drawable used for the track that the switch slides within. 479 * 480 * @param resId Resource ID of a track drawable 481 * 482 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_track 483 */ setTrackResource(int resId)484 public void setTrackResource(int resId) { 485 setTrackDrawable(mDrawableManager.getDrawable(getContext(), resId)); 486 } 487 488 /** 489 * Get the drawable used for the track that the switch slides within. 490 * 491 * @return Track drawable 492 * 493 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_track 494 */ getTrackDrawable()495 public Drawable getTrackDrawable() { 496 return mTrackDrawable; 497 } 498 499 /** 500 * Applies a tint to the track drawable. Does not modify the current 501 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 502 * <p> 503 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will 504 * automatically mutate the drawable and apply the specified tint and tint 505 * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}. 506 * 507 * @param tint the tint to apply, may be {@code null} to clear tint 508 * 509 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_trackTint 510 * @see #getTrackTintList() 511 */ setTrackTintList(@ullable ColorStateList tint)512 public void setTrackTintList(@Nullable ColorStateList tint) { 513 mTrackTintList = tint; 514 mHasTrackTint = true; 515 516 applyTrackTint(); 517 } 518 519 /** 520 * @return the tint applied to the track drawable 521 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_trackTint 522 * @see #setTrackTintList(ColorStateList) 523 */ 524 @Nullable getTrackTintList()525 public ColorStateList getTrackTintList() { 526 return mTrackTintList; 527 } 528 529 /** 530 * Specifies the blending mode used to apply the tint specified by 531 * {@link #setTrackTintList(ColorStateList)}} to the track drawable. 532 * The default mode is {@link PorterDuff.Mode#SRC_IN}. 533 * 534 * @param tintMode the blending mode used to apply the tint, may be 535 * {@code null} to clear tint 536 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_trackTintMode 537 * @see #getTrackTintMode() 538 */ setTrackTintMode(@ullable PorterDuff.Mode tintMode)539 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) { 540 mTrackTintMode = tintMode; 541 mHasTrackTintMode = true; 542 543 applyTrackTint(); 544 } 545 546 /** 547 * @return the blending mode used to apply the tint to the track 548 * drawable 549 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_trackTintMode 550 * @see #setTrackTintMode(PorterDuff.Mode) 551 */ 552 @Nullable getTrackTintMode()553 public PorterDuff.Mode getTrackTintMode() { 554 return mTrackTintMode; 555 } 556 applyTrackTint()557 private void applyTrackTint() { 558 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) { 559 mTrackDrawable = mTrackDrawable.mutate(); 560 561 if (mHasTrackTint) { 562 DrawableCompat.setTintList(mTrackDrawable, mTrackTintList); 563 } 564 565 if (mHasTrackTintMode) { 566 DrawableCompat.setTintMode(mTrackDrawable, mTrackTintMode); 567 } 568 569 // The drawable (or one of its children) may not have been 570 // stateful before applying the tint, so let's try again. 571 if (mTrackDrawable.isStateful()) { 572 mTrackDrawable.setState(getDrawableState()); 573 } 574 } 575 } 576 577 /** 578 * Set the drawable used for the switch "thumb" - the piece that the user 579 * can physically touch and drag along the track. 580 * 581 * @param thumb Thumb drawable 582 * 583 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_thumb 584 */ setThumbDrawable(Drawable thumb)585 public void setThumbDrawable(Drawable thumb) { 586 if (mThumbDrawable != null) { 587 mThumbDrawable.setCallback(null); 588 } 589 mThumbDrawable = thumb; 590 if (thumb != null) { 591 thumb.setCallback(this); 592 } 593 requestLayout(); 594 } 595 596 /** 597 * Set the drawable used for the switch "thumb" - the piece that the user 598 * can physically touch and drag along the track. 599 * 600 * @param resId Resource ID of a thumb drawable 601 * 602 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_thumb 603 */ setThumbResource(int resId)604 public void setThumbResource(int resId) { 605 setThumbDrawable(mDrawableManager.getDrawable(getContext(), resId)); 606 } 607 608 /** 609 * Get the drawable used for the switch "thumb" - the piece that the user 610 * can physically touch and drag along the track. 611 * 612 * @return Thumb drawable 613 * 614 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_thumb 615 */ getThumbDrawable()616 public Drawable getThumbDrawable() { 617 return mThumbDrawable; 618 } 619 620 /** 621 * Applies a tint to the thumb drawable. Does not modify the current 622 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 623 * <p> 624 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will 625 * automatically mutate the drawable and apply the specified tint and tint 626 * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}. 627 * 628 * @param tint the tint to apply, may be {@code null} to clear tint 629 * 630 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTint 631 * @see #getThumbTintList() 632 * @see Drawable#setTintList(ColorStateList) 633 */ setThumbTintList(@ullable ColorStateList tint)634 public void setThumbTintList(@Nullable ColorStateList tint) { 635 mThumbTintList = tint; 636 mHasThumbTint = true; 637 638 applyThumbTint(); 639 } 640 641 /** 642 * @return the tint applied to the thumb drawable 643 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTint 644 * @see #setThumbTintList(ColorStateList) 645 */ 646 @Nullable getThumbTintList()647 public ColorStateList getThumbTintList() { 648 return mThumbTintList; 649 } 650 651 /** 652 * Specifies the blending mode used to apply the tint specified by 653 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. 654 * The default mode is {@link PorterDuff.Mode#SRC_IN}. 655 * 656 * @param tintMode the blending mode used to apply the tint, may be 657 * {@code null} to clear tint 658 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTintMode 659 * @see #getThumbTintMode() 660 * @see Drawable#setTintMode(PorterDuff.Mode) 661 */ setThumbTintMode(@ullable PorterDuff.Mode tintMode)662 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { 663 mThumbTintMode = tintMode; 664 mHasThumbTintMode = true; 665 666 applyThumbTint(); 667 } 668 669 /** 670 * @return the blending mode used to apply the tint to the thumb 671 * drawable 672 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_thumbTintMode 673 * @see #setThumbTintMode(PorterDuff.Mode) 674 */ 675 @Nullable getThumbTintMode()676 public PorterDuff.Mode getThumbTintMode() { 677 return mThumbTintMode; 678 } 679 applyThumbTint()680 private void applyThumbTint() { 681 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) { 682 mThumbDrawable = mThumbDrawable.mutate(); 683 684 if (mHasThumbTint) { 685 DrawableCompat.setTintList(mThumbDrawable, mThumbTintList); 686 } 687 688 if (mHasThumbTintMode) { 689 DrawableCompat.setTintMode(mThumbDrawable, mThumbTintMode); 690 } 691 692 // The drawable (or one of its children) may not have been 693 // stateful before applying the tint, so let's try again. 694 if (mThumbDrawable.isStateful()) { 695 mThumbDrawable.setState(getDrawableState()); 696 } 697 } 698 } 699 700 /** 701 * Specifies whether the track should be split by the thumb. When true, 702 * the thumb's optical bounds will be clipped out of the track drawable, 703 * then the thumb will be drawn into the resulting gap. 704 * 705 * @param splitTrack Whether the track should be split by the thumb 706 * 707 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_splitTrack 708 */ setSplitTrack(boolean splitTrack)709 public void setSplitTrack(boolean splitTrack) { 710 mSplitTrack = splitTrack; 711 invalidate(); 712 } 713 714 /** 715 * Returns whether the track should be split by the thumb. 716 * 717 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_splitTrack 718 */ getSplitTrack()719 public boolean getSplitTrack() { 720 return mSplitTrack; 721 } 722 723 /** 724 * Returns the text displayed when the button is in the checked state. 725 * 726 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_textOn 727 */ getTextOn()728 public CharSequence getTextOn() { 729 return mTextOn; 730 } 731 732 /** 733 * Sets the text displayed when the button is in the checked state. 734 * 735 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_textOn 736 */ setTextOn(CharSequence textOn)737 public void setTextOn(CharSequence textOn) { 738 mTextOn = textOn; 739 requestLayout(); 740 } 741 742 /** 743 * Returns the text displayed when the button is not in the checked state. 744 * 745 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_textOff 746 */ getTextOff()747 public CharSequence getTextOff() { 748 return mTextOff; 749 } 750 751 /** 752 * Sets the text displayed when the button is not in the checked state. 753 * 754 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_android_textOff 755 */ setTextOff(CharSequence textOff)756 public void setTextOff(CharSequence textOff) { 757 mTextOff = textOff; 758 requestLayout(); 759 } 760 761 /** 762 * Sets whether the on/off text should be displayed. 763 * 764 * @param showText {@code true} to display on/off text 765 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_showText 766 */ setShowText(boolean showText)767 public void setShowText(boolean showText) { 768 if (mShowText != showText) { 769 mShowText = showText; 770 requestLayout(); 771 } 772 } 773 774 /** 775 * @return whether the on/off text should be displayed 776 * @attr ref android.support.v7.appcompat.R.styleable#SwitchCompat_showText 777 */ getShowText()778 public boolean getShowText() { 779 return mShowText; 780 } 781 782 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)783 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 784 if (mShowText) { 785 if (mOnLayout == null) { 786 mOnLayout = makeLayout(mTextOn); 787 } 788 789 if (mOffLayout == null) { 790 mOffLayout = makeLayout(mTextOff); 791 } 792 } 793 794 final Rect padding = mTempRect; 795 final int thumbWidth; 796 final int thumbHeight; 797 if (mThumbDrawable != null) { 798 // Cached thumb width does not include padding. 799 mThumbDrawable.getPadding(padding); 800 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right; 801 thumbHeight = mThumbDrawable.getIntrinsicHeight(); 802 } else { 803 thumbWidth = 0; 804 thumbHeight = 0; 805 } 806 807 final int maxTextWidth; 808 if (mShowText) { 809 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()) 810 + mThumbTextPadding * 2; 811 } else { 812 maxTextWidth = 0; 813 } 814 815 mThumbWidth = Math.max(maxTextWidth, thumbWidth); 816 817 final int trackHeight; 818 if (mTrackDrawable != null) { 819 mTrackDrawable.getPadding(padding); 820 trackHeight = mTrackDrawable.getIntrinsicHeight(); 821 } else { 822 padding.setEmpty(); 823 trackHeight = 0; 824 } 825 826 // Adjust left and right padding to ensure there's enough room for the 827 // thumb's padding (when present). 828 int paddingLeft = padding.left; 829 int paddingRight = padding.right; 830 if (mThumbDrawable != null) { 831 final Rect inset = DrawableUtils.getOpticalBounds(mThumbDrawable); 832 paddingLeft = Math.max(paddingLeft, inset.left); 833 paddingRight = Math.max(paddingRight, inset.right); 834 } 835 836 final int switchWidth = Math.max(mSwitchMinWidth, 837 2 * mThumbWidth + paddingLeft + paddingRight); 838 final int switchHeight = Math.max(trackHeight, thumbHeight); 839 mSwitchWidth = switchWidth; 840 mSwitchHeight = switchHeight; 841 842 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 843 844 final int measuredHeight = getMeasuredHeight(); 845 if (measuredHeight < switchHeight) { 846 setMeasuredDimension(ViewCompat.getMeasuredWidthAndState(this), switchHeight); 847 } 848 } 849 850 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 851 @Override onPopulateAccessibilityEvent(AccessibilityEvent event)852 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 853 super.onPopulateAccessibilityEvent(event); 854 855 final CharSequence text = isChecked() ? mTextOn : mTextOff; 856 if (text != null) { 857 event.getText().add(text); 858 } 859 } 860 makeLayout(CharSequence text)861 private Layout makeLayout(CharSequence text) { 862 final CharSequence transformed = (mSwitchTransformationMethod != null) 863 ? mSwitchTransformationMethod.getTransformation(text, this) 864 : text; 865 866 return new StaticLayout(transformed, mTextPaint, 867 transformed != null ? 868 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)) : 0, 869 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true); 870 } 871 872 /** 873 * @return true if (x, y) is within the target area of the switch thumb 874 */ hitThumb(float x, float y)875 private boolean hitThumb(float x, float y) { 876 if (mThumbDrawable == null) { 877 return false; 878 } 879 880 // Relies on mTempRect, MUST be called first! 881 final int thumbOffset = getThumbOffset(); 882 883 mThumbDrawable.getPadding(mTempRect); 884 final int thumbTop = mSwitchTop - mTouchSlop; 885 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop; 886 final int thumbRight = thumbLeft + mThumbWidth + 887 mTempRect.left + mTempRect.right + mTouchSlop; 888 final int thumbBottom = mSwitchBottom + mTouchSlop; 889 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom; 890 } 891 892 @Override onTouchEvent(MotionEvent ev)893 public boolean onTouchEvent(MotionEvent ev) { 894 mVelocityTracker.addMovement(ev); 895 final int action = MotionEventCompat.getActionMasked(ev); 896 switch (action) { 897 case MotionEvent.ACTION_DOWN: { 898 final float x = ev.getX(); 899 final float y = ev.getY(); 900 if (isEnabled() && hitThumb(x, y)) { 901 mTouchMode = TOUCH_MODE_DOWN; 902 mTouchX = x; 903 mTouchY = y; 904 } 905 break; 906 } 907 908 case MotionEvent.ACTION_MOVE: { 909 switch (mTouchMode) { 910 case TOUCH_MODE_IDLE: 911 // Didn't target the thumb, treat normally. 912 break; 913 914 case TOUCH_MODE_DOWN: { 915 final float x = ev.getX(); 916 final float y = ev.getY(); 917 if (Math.abs(x - mTouchX) > mTouchSlop || 918 Math.abs(y - mTouchY) > mTouchSlop) { 919 mTouchMode = TOUCH_MODE_DRAGGING; 920 getParent().requestDisallowInterceptTouchEvent(true); 921 mTouchX = x; 922 mTouchY = y; 923 return true; 924 } 925 break; 926 } 927 928 case TOUCH_MODE_DRAGGING: { 929 final float x = ev.getX(); 930 final int thumbScrollRange = getThumbScrollRange(); 931 final float thumbScrollOffset = x - mTouchX; 932 float dPos; 933 if (thumbScrollRange != 0) { 934 dPos = thumbScrollOffset / thumbScrollRange; 935 } else { 936 // If the thumb scroll range is empty, just use the 937 // movement direction to snap on or off. 938 dPos = thumbScrollOffset > 0 ? 1 : -1; 939 } 940 if (ViewUtils.isLayoutRtl(this)) { 941 dPos = -dPos; 942 } 943 final float newPos = constrain(mThumbPosition + dPos, 0, 1); 944 if (newPos != mThumbPosition) { 945 mTouchX = x; 946 setThumbPosition(newPos); 947 } 948 return true; 949 } 950 } 951 break; 952 } 953 954 case MotionEvent.ACTION_UP: 955 case MotionEvent.ACTION_CANCEL: { 956 if (mTouchMode == TOUCH_MODE_DRAGGING) { 957 stopDrag(ev); 958 // Allow super class to handle pressed state, etc. 959 super.onTouchEvent(ev); 960 return true; 961 } 962 mTouchMode = TOUCH_MODE_IDLE; 963 mVelocityTracker.clear(); 964 break; 965 } 966 } 967 968 return super.onTouchEvent(ev); 969 } 970 cancelSuperTouch(MotionEvent ev)971 private void cancelSuperTouch(MotionEvent ev) { 972 MotionEvent cancel = MotionEvent.obtain(ev); 973 cancel.setAction(MotionEvent.ACTION_CANCEL); 974 super.onTouchEvent(cancel); 975 cancel.recycle(); 976 } 977 978 /** 979 * Called from onTouchEvent to end a drag operation. 980 * 981 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL 982 */ stopDrag(MotionEvent ev)983 private void stopDrag(MotionEvent ev) { 984 mTouchMode = TOUCH_MODE_IDLE; 985 986 // Commit the change if the event is up and not canceled and the switch 987 // has not been disabled during the drag. 988 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); 989 final boolean oldState = isChecked(); 990 final boolean newState; 991 if (commitChange) { 992 mVelocityTracker.computeCurrentVelocity(1000); 993 final float xvel = mVelocityTracker.getXVelocity(); 994 if (Math.abs(xvel) > mMinFlingVelocity) { 995 newState = ViewUtils.isLayoutRtl(this) ? (xvel < 0) : (xvel > 0); 996 } else { 997 newState = getTargetCheckedState(); 998 } 999 } else { 1000 newState = oldState; 1001 } 1002 1003 if (newState != oldState) { 1004 playSoundEffect(SoundEffectConstants.CLICK); 1005 } 1006 // Always call setChecked so that the thumb is moved back to the correct edge 1007 setChecked(newState); 1008 cancelSuperTouch(ev); 1009 } 1010 animateThumbToCheckedState(final boolean newCheckedState)1011 private void animateThumbToCheckedState(final boolean newCheckedState) { 1012 if (mPositionAnimator != null) { 1013 // If there's a current animator running, cancel it 1014 cancelPositionAnimator(); 1015 } 1016 1017 mPositionAnimator = new ThumbAnimation(mThumbPosition, newCheckedState ? 1f : 0f); 1018 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); 1019 mPositionAnimator.setAnimationListener(new Animation.AnimationListener() { 1020 @Override 1021 public void onAnimationStart(Animation animation) {} 1022 1023 @Override 1024 public void onAnimationEnd(Animation animation) { 1025 if (mPositionAnimator == animation) { 1026 // If we're still the active animation, ensure the final position 1027 setThumbPosition(newCheckedState ? 1f : 0f); 1028 mPositionAnimator = null; 1029 } 1030 } 1031 1032 @Override 1033 public void onAnimationRepeat(Animation animation) {} 1034 }); 1035 startAnimation(mPositionAnimator); 1036 } 1037 cancelPositionAnimator()1038 private void cancelPositionAnimator() { 1039 if (mPositionAnimator != null) { 1040 clearAnimation(); 1041 mPositionAnimator = null; 1042 } 1043 } 1044 getTargetCheckedState()1045 private boolean getTargetCheckedState() { 1046 return mThumbPosition > 0.5f; 1047 } 1048 1049 /** 1050 * Sets the thumb position as a decimal value between 0 (off) and 1 (on). 1051 * 1052 * @param position new position between [0,1] 1053 */ setThumbPosition(float position)1054 private void setThumbPosition(float position) { 1055 mThumbPosition = position; 1056 invalidate(); 1057 } 1058 1059 @Override toggle()1060 public void toggle() { 1061 setChecked(!isChecked()); 1062 } 1063 1064 @Override setChecked(boolean checked)1065 public void setChecked(boolean checked) { 1066 super.setChecked(checked); 1067 1068 // Calling the super method may result in setChecked() getting called 1069 // recursively with a different value, so load the REAL value... 1070 checked = isChecked(); 1071 1072 if (getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown()) { 1073 animateThumbToCheckedState(checked); 1074 } else { 1075 // Immediately move the thumb to the new position. 1076 cancelPositionAnimator(); 1077 setThumbPosition(checked ? 1 : 0); 1078 } 1079 } 1080 1081 @Override onLayout(boolean changed, int left, int top, int right, int bottom)1082 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1083 super.onLayout(changed, left, top, right, bottom); 1084 1085 int opticalInsetLeft = 0; 1086 int opticalInsetRight = 0; 1087 if (mThumbDrawable != null) { 1088 final Rect trackPadding = mTempRect; 1089 if (mTrackDrawable != null) { 1090 mTrackDrawable.getPadding(trackPadding); 1091 } else { 1092 trackPadding.setEmpty(); 1093 } 1094 1095 final Rect insets = DrawableUtils.getOpticalBounds(mThumbDrawable); 1096 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left); 1097 opticalInsetRight = Math.max(0, insets.right - trackPadding.right); 1098 } 1099 1100 final int switchRight; 1101 final int switchLeft; 1102 if (ViewUtils.isLayoutRtl(this)) { 1103 switchLeft = getPaddingLeft() + opticalInsetLeft; 1104 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight; 1105 } else { 1106 switchRight = getWidth() - getPaddingRight() - opticalInsetRight; 1107 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight; 1108 } 1109 1110 final int switchTop; 1111 final int switchBottom; 1112 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { 1113 default: 1114 case Gravity.TOP: 1115 switchTop = getPaddingTop(); 1116 switchBottom = switchTop + mSwitchHeight; 1117 break; 1118 1119 case Gravity.CENTER_VERTICAL: 1120 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - 1121 mSwitchHeight / 2; 1122 switchBottom = switchTop + mSwitchHeight; 1123 break; 1124 1125 case Gravity.BOTTOM: 1126 switchBottom = getHeight() - getPaddingBottom(); 1127 switchTop = switchBottom - mSwitchHeight; 1128 break; 1129 } 1130 1131 mSwitchLeft = switchLeft; 1132 mSwitchTop = switchTop; 1133 mSwitchBottom = switchBottom; 1134 mSwitchRight = switchRight; 1135 } 1136 1137 @Override draw(Canvas c)1138 public void draw(Canvas c) { 1139 final Rect padding = mTempRect; 1140 final int switchLeft = mSwitchLeft; 1141 final int switchTop = mSwitchTop; 1142 final int switchRight = mSwitchRight; 1143 final int switchBottom = mSwitchBottom; 1144 1145 int thumbInitialLeft = switchLeft + getThumbOffset(); 1146 1147 final Rect thumbInsets; 1148 if (mThumbDrawable != null) { 1149 thumbInsets = DrawableUtils.getOpticalBounds(mThumbDrawable); 1150 } else { 1151 thumbInsets = DrawableUtils.INSETS_NONE; 1152 } 1153 1154 // Layout the track. 1155 if (mTrackDrawable != null) { 1156 mTrackDrawable.getPadding(padding); 1157 1158 // Adjust thumb position for track padding. 1159 thumbInitialLeft += padding.left; 1160 1161 // If necessary, offset by the optical insets of the thumb asset. 1162 int trackLeft = switchLeft; 1163 int trackTop = switchTop; 1164 int trackRight = switchRight; 1165 int trackBottom = switchBottom; 1166 if (thumbInsets != null) { 1167 if (thumbInsets.left > padding.left) { 1168 trackLeft += thumbInsets.left - padding.left; 1169 } 1170 if (thumbInsets.top > padding.top) { 1171 trackTop += thumbInsets.top - padding.top; 1172 } 1173 if (thumbInsets.right > padding.right) { 1174 trackRight -= thumbInsets.right - padding.right; 1175 } 1176 if (thumbInsets.bottom > padding.bottom) { 1177 trackBottom -= thumbInsets.bottom - padding.bottom; 1178 } 1179 } 1180 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom); 1181 } 1182 1183 // Layout the thumb. 1184 if (mThumbDrawable != null) { 1185 mThumbDrawable.getPadding(padding); 1186 1187 final int thumbLeft = thumbInitialLeft - padding.left; 1188 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right; 1189 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); 1190 1191 final Drawable background = getBackground(); 1192 if (background != null) { 1193 DrawableCompat.setHotspotBounds(background, thumbLeft, switchTop, 1194 thumbRight, switchBottom); 1195 } 1196 } 1197 1198 // Draw the background. 1199 super.draw(c); 1200 } 1201 1202 @Override onDraw(Canvas canvas)1203 protected void onDraw(Canvas canvas) { 1204 super.onDraw(canvas); 1205 1206 final Rect padding = mTempRect; 1207 final Drawable trackDrawable = mTrackDrawable; 1208 if (trackDrawable != null) { 1209 trackDrawable.getPadding(padding); 1210 } else { 1211 padding.setEmpty(); 1212 } 1213 1214 final int switchTop = mSwitchTop; 1215 final int switchBottom = mSwitchBottom; 1216 final int switchInnerTop = switchTop + padding.top; 1217 final int switchInnerBottom = switchBottom - padding.bottom; 1218 1219 final Drawable thumbDrawable = mThumbDrawable; 1220 if (trackDrawable != null) { 1221 if (mSplitTrack && thumbDrawable != null) { 1222 final Rect insets = DrawableUtils.getOpticalBounds(thumbDrawable); 1223 thumbDrawable.copyBounds(padding); 1224 padding.left += insets.left; 1225 padding.right -= insets.right; 1226 1227 final int saveCount = canvas.save(); 1228 canvas.clipRect(padding, Region.Op.DIFFERENCE); 1229 trackDrawable.draw(canvas); 1230 canvas.restoreToCount(saveCount); 1231 } else { 1232 trackDrawable.draw(canvas); 1233 } 1234 } 1235 1236 final int saveCount = canvas.save(); 1237 1238 if (thumbDrawable != null) { 1239 thumbDrawable.draw(canvas); 1240 } 1241 1242 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; 1243 if (switchText != null) { 1244 final int drawableState[] = getDrawableState(); 1245 if (mTextColors != null) { 1246 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); 1247 } 1248 mTextPaint.drawableState = drawableState; 1249 1250 final int cX; 1251 if (thumbDrawable != null) { 1252 final Rect bounds = thumbDrawable.getBounds(); 1253 cX = bounds.left + bounds.right; 1254 } else { 1255 cX = getWidth(); 1256 } 1257 1258 final int left = cX / 2 - switchText.getWidth() / 2; 1259 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; 1260 canvas.translate(left, top); 1261 switchText.draw(canvas); 1262 } 1263 1264 canvas.restoreToCount(saveCount); 1265 } 1266 1267 @Override getCompoundPaddingLeft()1268 public int getCompoundPaddingLeft() { 1269 if (!ViewUtils.isLayoutRtl(this)) { 1270 return super.getCompoundPaddingLeft(); 1271 } 1272 int padding = super.getCompoundPaddingLeft() + mSwitchWidth; 1273 if (!TextUtils.isEmpty(getText())) { 1274 padding += mSwitchPadding; 1275 } 1276 return padding; 1277 } 1278 1279 @Override getCompoundPaddingRight()1280 public int getCompoundPaddingRight() { 1281 if (ViewUtils.isLayoutRtl(this)) { 1282 return super.getCompoundPaddingRight(); 1283 } 1284 int padding = super.getCompoundPaddingRight() + mSwitchWidth; 1285 if (!TextUtils.isEmpty(getText())) { 1286 padding += mSwitchPadding; 1287 } 1288 return padding; 1289 } 1290 1291 /** 1292 * Translates thumb position to offset according to current RTL setting and 1293 * thumb scroll range. Accounts for both track and thumb padding. 1294 * 1295 * @return thumb offset 1296 */ getThumbOffset()1297 private int getThumbOffset() { 1298 final float thumbPosition; 1299 if (ViewUtils.isLayoutRtl(this)) { 1300 thumbPosition = 1 - mThumbPosition; 1301 } else { 1302 thumbPosition = mThumbPosition; 1303 } 1304 return (int) (thumbPosition * getThumbScrollRange() + 0.5f); 1305 } 1306 getThumbScrollRange()1307 private int getThumbScrollRange() { 1308 if (mTrackDrawable != null) { 1309 final Rect padding = mTempRect; 1310 mTrackDrawable.getPadding(padding); 1311 1312 final Rect insets; 1313 if (mThumbDrawable != null) { 1314 insets = DrawableUtils.getOpticalBounds(mThumbDrawable); 1315 } else { 1316 insets = DrawableUtils.INSETS_NONE; 1317 } 1318 1319 return mSwitchWidth - mThumbWidth - padding.left - padding.right 1320 - insets.left - insets.right; 1321 } else { 1322 return 0; 1323 } 1324 } 1325 1326 @Override onCreateDrawableState(int extraSpace)1327 protected int[] onCreateDrawableState(int extraSpace) { 1328 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 1329 if (isChecked()) { 1330 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 1331 } 1332 return drawableState; 1333 } 1334 1335 @Override drawableStateChanged()1336 protected void drawableStateChanged() { 1337 super.drawableStateChanged(); 1338 1339 final int[] state = getDrawableState(); 1340 boolean changed = false; 1341 1342 final Drawable thumbDrawable = mThumbDrawable; 1343 if (thumbDrawable != null && thumbDrawable.isStateful()) { 1344 changed |= thumbDrawable.setState(state); 1345 } 1346 1347 final Drawable trackDrawable = mTrackDrawable; 1348 if (trackDrawable != null && trackDrawable.isStateful()) { 1349 changed |= trackDrawable.setState(state); 1350 } 1351 1352 if (changed) { 1353 invalidate(); 1354 } 1355 } 1356 1357 @Override drawableHotspotChanged(float x, float y)1358 public void drawableHotspotChanged(float x, float y) { 1359 if (Build.VERSION.SDK_INT >= 21) { 1360 super.drawableHotspotChanged(x, y); 1361 } 1362 1363 if (mThumbDrawable != null) { 1364 DrawableCompat.setHotspot(mThumbDrawable, x, y); 1365 } 1366 1367 if (mTrackDrawable != null) { 1368 DrawableCompat.setHotspot(mTrackDrawable, x, y); 1369 } 1370 } 1371 1372 @Override verifyDrawable(Drawable who)1373 protected boolean verifyDrawable(Drawable who) { 1374 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; 1375 } 1376 1377 @Override jumpDrawablesToCurrentState()1378 public void jumpDrawablesToCurrentState() { 1379 if (Build.VERSION.SDK_INT >= 11) { 1380 super.jumpDrawablesToCurrentState(); 1381 1382 if (mThumbDrawable != null) { 1383 mThumbDrawable.jumpToCurrentState(); 1384 } 1385 1386 if (mTrackDrawable != null) { 1387 mTrackDrawable.jumpToCurrentState(); 1388 } 1389 1390 cancelPositionAnimator(); 1391 setThumbPosition(isChecked() ? 1 : 0); 1392 } 1393 } 1394 1395 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1396 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1397 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1398 super.onInitializeAccessibilityEvent(event); 1399 event.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME); 1400 } 1401 1402 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1403 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1404 if (Build.VERSION.SDK_INT >= 14) { 1405 super.onInitializeAccessibilityNodeInfo(info); 1406 info.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME); 1407 CharSequence switchText = isChecked() ? mTextOn : mTextOff; 1408 if (!TextUtils.isEmpty(switchText)) { 1409 CharSequence oldText = info.getText(); 1410 if (TextUtils.isEmpty(oldText)) { 1411 info.setText(switchText); 1412 } else { 1413 StringBuilder newText = new StringBuilder(); 1414 newText.append(oldText).append(' ').append(switchText); 1415 info.setText(newText); 1416 } 1417 } 1418 } 1419 } 1420 1421 /** 1422 * Taken from android.util.MathUtils 1423 */ constrain(float amount, float low, float high)1424 private static float constrain(float amount, float low, float high) { 1425 return amount < low ? low : (amount > high ? high : amount); 1426 } 1427 1428 private class ThumbAnimation extends Animation { 1429 final float mStartPosition; 1430 final float mEndPosition; 1431 final float mDiff; 1432 ThumbAnimation(float startPosition, float endPosition)1433 private ThumbAnimation(float startPosition, float endPosition) { 1434 mStartPosition = startPosition; 1435 mEndPosition = endPosition; 1436 mDiff = endPosition - startPosition; 1437 } 1438 1439 @Override applyTransformation(float interpolatedTime, Transformation t)1440 protected void applyTransformation(float interpolatedTime, Transformation t) { 1441 setThumbPosition(mStartPosition + (mDiff * interpolatedTime)); 1442 } 1443 } 1444 }