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