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