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