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