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