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