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.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Insets; 26 import android.graphics.Paint; 27 import android.graphics.Rect; 28 import android.graphics.Typeface; 29 import android.graphics.Region.Op; 30 import android.graphics.drawable.Drawable; 31 import android.text.Layout; 32 import android.text.StaticLayout; 33 import android.text.TextPaint; 34 import android.text.TextUtils; 35 import android.text.method.AllCapsTransformationMethod; 36 import android.text.method.TransformationMethod2; 37 import android.util.AttributeSet; 38 import android.util.FloatProperty; 39 import android.util.MathUtils; 40 import android.view.Gravity; 41 import android.view.MotionEvent; 42 import android.view.SoundEffectConstants; 43 import android.view.VelocityTracker; 44 import android.view.ViewConfiguration; 45 import android.view.accessibility.AccessibilityEvent; 46 import android.view.accessibility.AccessibilityNodeInfo; 47 48 import com.android.internal.R; 49 50 /** 51 * A Switch is a two-state toggle switch widget that can select between two 52 * options. The user may drag the "thumb" back and forth to choose the selected option, 53 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text} 54 * property controls the text displayed in the label for the switch, whereas the 55 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text 56 * controls the text on the thumb. Similarly, the 57 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related 58 * setTypeface() methods control the typeface and style of label text, whereas the 59 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and 60 * the related seSwitchTypeface() methods control that of the thumb. 61 * 62 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a> 63 * guide.</p> 64 * 65 * @attr ref android.R.styleable#Switch_textOn 66 * @attr ref android.R.styleable#Switch_textOff 67 * @attr ref android.R.styleable#Switch_switchMinWidth 68 * @attr ref android.R.styleable#Switch_switchPadding 69 * @attr ref android.R.styleable#Switch_switchTextAppearance 70 * @attr ref android.R.styleable#Switch_thumb 71 * @attr ref android.R.styleable#Switch_thumbTextPadding 72 * @attr ref android.R.styleable#Switch_track 73 */ 74 public class Switch extends CompoundButton { 75 private static final int THUMB_ANIMATION_DURATION = 250; 76 77 private static final int TOUCH_MODE_IDLE = 0; 78 private static final int TOUCH_MODE_DOWN = 1; 79 private static final int TOUCH_MODE_DRAGGING = 2; 80 81 // Enum for the "typeface" XML parameter. 82 private static final int SANS = 1; 83 private static final int SERIF = 2; 84 private static final int MONOSPACE = 3; 85 86 private Drawable mThumbDrawable; 87 private Drawable mTrackDrawable; 88 private int mThumbTextPadding; 89 private int mSwitchMinWidth; 90 private int mSwitchPadding; 91 private boolean mSplitTrack; 92 private CharSequence mTextOn; 93 private CharSequence mTextOff; 94 private boolean mShowText; 95 96 private int mTouchMode; 97 private int mTouchSlop; 98 private float mTouchX; 99 private float mTouchY; 100 private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 101 private int mMinFlingVelocity; 102 103 private float mThumbPosition; 104 105 /** 106 * Width required to draw the switch track and thumb. Includes padding and 107 * optical bounds for both the track and thumb. 108 */ 109 private int mSwitchWidth; 110 111 /** 112 * Height required to draw the switch track and thumb. Includes padding and 113 * optical bounds for both the track and thumb. 114 */ 115 private int mSwitchHeight; 116 117 /** 118 * Width of the thumb's content region. Does not include padding or 119 * optical bounds. 120 */ 121 private int mThumbWidth; 122 123 /** Left bound for drawing the switch track and thumb. */ 124 private int mSwitchLeft; 125 126 /** Top bound for drawing the switch track and thumb. */ 127 private int mSwitchTop; 128 129 /** Right bound for drawing the switch track and thumb. */ 130 private int mSwitchRight; 131 132 /** Bottom bound for drawing the switch track and thumb. */ 133 private int mSwitchBottom; 134 135 private TextPaint mTextPaint; 136 private ColorStateList mTextColors; 137 private Layout mOnLayout; 138 private Layout mOffLayout; 139 private TransformationMethod2 mSwitchTransformationMethod; 140 private ObjectAnimator mPositionAnimator; 141 142 @SuppressWarnings("hiding") 143 private final Rect mTempRect = new Rect(); 144 145 private static final int[] CHECKED_STATE_SET = { 146 R.attr.state_checked 147 }; 148 149 /** 150 * Construct a new Switch with default styling. 151 * 152 * @param context The Context that will determine this widget's theming. 153 */ Switch(Context context)154 public Switch(Context context) { 155 this(context, null); 156 } 157 158 /** 159 * Construct a new Switch with default styling, overriding specific style 160 * attributes as requested. 161 * 162 * @param context The Context that will determine this widget's theming. 163 * @param attrs Specification of attributes that should deviate from default styling. 164 */ Switch(Context context, AttributeSet attrs)165 public Switch(Context context, AttributeSet attrs) { 166 this(context, attrs, com.android.internal.R.attr.switchStyle); 167 } 168 169 /** 170 * Construct a new Switch with a default style determined by the given theme attribute, 171 * overriding specific style attributes as requested. 172 * 173 * @param context The Context that will determine this widget's theming. 174 * @param attrs Specification of attributes that should deviate from the default styling. 175 * @param defStyleAttr An attribute in the current theme that contains a 176 * reference to a style resource that supplies default values for 177 * the view. Can be 0 to not look for defaults. 178 */ Switch(Context context, AttributeSet attrs, int defStyleAttr)179 public Switch(Context context, AttributeSet attrs, int defStyleAttr) { 180 this(context, attrs, defStyleAttr, 0); 181 } 182 183 184 /** 185 * Construct a new Switch with a default style determined by the given theme 186 * attribute or style resource, overriding specific style attributes as 187 * requested. 188 * 189 * @param context The Context that will determine this widget's theming. 190 * @param attrs Specification of attributes that should deviate from the 191 * default styling. 192 * @param defStyleAttr An attribute in the current theme that contains a 193 * reference to a style resource that supplies default values for 194 * the view. Can be 0 to not look for defaults. 195 * @param defStyleRes A resource identifier of a style resource that 196 * supplies default values for the view, used only if 197 * defStyleAttr is 0 or can not be found in the theme. Can be 0 198 * to not look for defaults. 199 */ Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)200 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 201 super(context, attrs, defStyleAttr, defStyleRes); 202 203 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 204 205 final Resources res = getResources(); 206 mTextPaint.density = res.getDisplayMetrics().density; 207 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); 208 209 final TypedArray a = context.obtainStyledAttributes( 210 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes); 211 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb); 212 if (mThumbDrawable != null) { 213 mThumbDrawable.setCallback(this); 214 } 215 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track); 216 if (mTrackDrawable != null) { 217 mTrackDrawable.setCallback(this); 218 } 219 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn); 220 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff); 221 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true); 222 mThumbTextPadding = a.getDimensionPixelSize( 223 com.android.internal.R.styleable.Switch_thumbTextPadding, 0); 224 mSwitchMinWidth = a.getDimensionPixelSize( 225 com.android.internal.R.styleable.Switch_switchMinWidth, 0); 226 mSwitchPadding = a.getDimensionPixelSize( 227 com.android.internal.R.styleable.Switch_switchPadding, 0); 228 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false); 229 230 final int appearance = a.getResourceId( 231 com.android.internal.R.styleable.Switch_switchTextAppearance, 0); 232 if (appearance != 0) { 233 setSwitchTextAppearance(context, appearance); 234 } 235 a.recycle(); 236 237 final ViewConfiguration config = ViewConfiguration.get(context); 238 mTouchSlop = config.getScaledTouchSlop(); 239 mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); 240 241 // Refresh display with current params 242 refreshDrawableState(); 243 setChecked(isChecked()); 244 } 245 246 /** 247 * Sets the switch text color, size, style, hint color, and highlight color 248 * from the specified TextAppearance resource. 249 * 250 * @attr ref android.R.styleable#Switch_switchTextAppearance 251 */ setSwitchTextAppearance(Context context, int resid)252 public void setSwitchTextAppearance(Context context, int resid) { 253 TypedArray appearance = 254 context.obtainStyledAttributes(resid, 255 com.android.internal.R.styleable.TextAppearance); 256 257 ColorStateList colors; 258 int ts; 259 260 colors = appearance.getColorStateList(com.android.internal.R.styleable. 261 TextAppearance_textColor); 262 if (colors != null) { 263 mTextColors = colors; 264 } else { 265 // If no color set in TextAppearance, default to the view's textColor 266 mTextColors = getTextColors(); 267 } 268 269 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. 270 TextAppearance_textSize, 0); 271 if (ts != 0) { 272 if (ts != mTextPaint.getTextSize()) { 273 mTextPaint.setTextSize(ts); 274 requestLayout(); 275 } 276 } 277 278 int typefaceIndex, styleIndex; 279 280 typefaceIndex = appearance.getInt(com.android.internal.R.styleable. 281 TextAppearance_typeface, -1); 282 styleIndex = appearance.getInt(com.android.internal.R.styleable. 283 TextAppearance_textStyle, -1); 284 285 setSwitchTypefaceByIndex(typefaceIndex, styleIndex); 286 287 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable. 288 TextAppearance_textAllCaps, false); 289 if (allCaps) { 290 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext()); 291 mSwitchTransformationMethod.setLengthChangesAllowed(true); 292 } else { 293 mSwitchTransformationMethod = null; 294 } 295 296 appearance.recycle(); 297 } 298 setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex)299 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) { 300 Typeface tf = null; 301 switch (typefaceIndex) { 302 case SANS: 303 tf = Typeface.SANS_SERIF; 304 break; 305 306 case SERIF: 307 tf = Typeface.SERIF; 308 break; 309 310 case MONOSPACE: 311 tf = Typeface.MONOSPACE; 312 break; 313 } 314 315 setSwitchTypeface(tf, styleIndex); 316 } 317 318 /** 319 * Sets the typeface and style in which the text should be displayed on the 320 * switch, and turns on the fake bold and italic bits in the Paint if the 321 * Typeface that you provided does not have all the bits in the 322 * style that you specified. 323 */ setSwitchTypeface(Typeface tf, int style)324 public void setSwitchTypeface(Typeface tf, int style) { 325 if (style > 0) { 326 if (tf == null) { 327 tf = Typeface.defaultFromStyle(style); 328 } else { 329 tf = Typeface.create(tf, style); 330 } 331 332 setSwitchTypeface(tf); 333 // now compute what (if any) algorithmic styling is needed 334 int typefaceStyle = tf != null ? tf.getStyle() : 0; 335 int need = style & ~typefaceStyle; 336 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); 337 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); 338 } else { 339 mTextPaint.setFakeBoldText(false); 340 mTextPaint.setTextSkewX(0); 341 setSwitchTypeface(tf); 342 } 343 } 344 345 /** 346 * Sets the typeface in which the text should be displayed on the switch. 347 * Note that not all Typeface families actually have bold and italic 348 * variants, so you may need to use 349 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance 350 * that you actually want. 351 * 352 * @attr ref android.R.styleable#TextView_typeface 353 * @attr ref android.R.styleable#TextView_textStyle 354 */ setSwitchTypeface(Typeface tf)355 public void setSwitchTypeface(Typeface tf) { 356 if (mTextPaint.getTypeface() != tf) { 357 mTextPaint.setTypeface(tf); 358 359 requestLayout(); 360 invalidate(); 361 } 362 } 363 364 /** 365 * Set the amount of horizontal padding between the switch and the associated text. 366 * 367 * @param pixels Amount of padding in pixels 368 * 369 * @attr ref android.R.styleable#Switch_switchPadding 370 */ setSwitchPadding(int pixels)371 public void setSwitchPadding(int pixels) { 372 mSwitchPadding = pixels; 373 requestLayout(); 374 } 375 376 /** 377 * Get the amount of horizontal padding between the switch and the associated text. 378 * 379 * @return Amount of padding in pixels 380 * 381 * @attr ref android.R.styleable#Switch_switchPadding 382 */ getSwitchPadding()383 public int getSwitchPadding() { 384 return mSwitchPadding; 385 } 386 387 /** 388 * Set the minimum width of the switch in pixels. The switch's width will be the maximum 389 * of this value and its measured width as determined by the switch drawables and text used. 390 * 391 * @param pixels Minimum width of the switch in pixels 392 * 393 * @attr ref android.R.styleable#Switch_switchMinWidth 394 */ setSwitchMinWidth(int pixels)395 public void setSwitchMinWidth(int pixels) { 396 mSwitchMinWidth = pixels; 397 requestLayout(); 398 } 399 400 /** 401 * Get the minimum width of the switch in pixels. The switch's width will be the maximum 402 * of this value and its measured width as determined by the switch drawables and text used. 403 * 404 * @return Minimum width of the switch in pixels 405 * 406 * @attr ref android.R.styleable#Switch_switchMinWidth 407 */ getSwitchMinWidth()408 public int getSwitchMinWidth() { 409 return mSwitchMinWidth; 410 } 411 412 /** 413 * Set the horizontal padding around the text drawn on the switch itself. 414 * 415 * @param pixels Horizontal padding for switch thumb text in pixels 416 * 417 * @attr ref android.R.styleable#Switch_thumbTextPadding 418 */ setThumbTextPadding(int pixels)419 public void setThumbTextPadding(int pixels) { 420 mThumbTextPadding = pixels; 421 requestLayout(); 422 } 423 424 /** 425 * Get the horizontal padding around the text drawn on the switch itself. 426 * 427 * @return Horizontal padding for switch thumb text in pixels 428 * 429 * @attr ref android.R.styleable#Switch_thumbTextPadding 430 */ getThumbTextPadding()431 public int getThumbTextPadding() { 432 return mThumbTextPadding; 433 } 434 435 /** 436 * Set the drawable used for the track that the switch slides within. 437 * 438 * @param track Track drawable 439 * 440 * @attr ref android.R.styleable#Switch_track 441 */ setTrackDrawable(Drawable track)442 public void setTrackDrawable(Drawable track) { 443 if (mTrackDrawable != null) { 444 mTrackDrawable.setCallback(null); 445 } 446 mTrackDrawable = track; 447 if (track != null) { 448 track.setCallback(this); 449 } 450 requestLayout(); 451 } 452 453 /** 454 * Set the drawable used for the track that the switch slides within. 455 * 456 * @param resId Resource ID of a track drawable 457 * 458 * @attr ref android.R.styleable#Switch_track 459 */ setTrackResource(int resId)460 public void setTrackResource(int resId) { 461 setTrackDrawable(getContext().getDrawable(resId)); 462 } 463 464 /** 465 * Get the drawable used for the track that the switch slides within. 466 * 467 * @return Track drawable 468 * 469 * @attr ref android.R.styleable#Switch_track 470 */ getTrackDrawable()471 public Drawable getTrackDrawable() { 472 return mTrackDrawable; 473 } 474 475 /** 476 * Set the drawable used for the switch "thumb" - the piece that the user 477 * can physically touch and drag along the track. 478 * 479 * @param thumb Thumb drawable 480 * 481 * @attr ref android.R.styleable#Switch_thumb 482 */ setThumbDrawable(Drawable thumb)483 public void setThumbDrawable(Drawable thumb) { 484 if (mThumbDrawable != null) { 485 mThumbDrawable.setCallback(null); 486 } 487 mThumbDrawable = thumb; 488 if (thumb != null) { 489 thumb.setCallback(this); 490 } 491 requestLayout(); 492 } 493 494 /** 495 * Set the drawable used for the switch "thumb" - the piece that the user 496 * can physically touch and drag along the track. 497 * 498 * @param resId Resource ID of a thumb drawable 499 * 500 * @attr ref android.R.styleable#Switch_thumb 501 */ setThumbResource(int resId)502 public void setThumbResource(int resId) { 503 setThumbDrawable(getContext().getDrawable(resId)); 504 } 505 506 /** 507 * Get the drawable used for the switch "thumb" - the piece that the user 508 * can physically touch and drag along the track. 509 * 510 * @return Thumb drawable 511 * 512 * @attr ref android.R.styleable#Switch_thumb 513 */ getThumbDrawable()514 public Drawable getThumbDrawable() { 515 return mThumbDrawable; 516 } 517 518 /** 519 * Specifies whether the track should be split by the thumb. When true, 520 * the thumb's optical bounds will be clipped out of the track drawable, 521 * then the thumb will be drawn into the resulting gap. 522 * 523 * @param splitTrack Whether the track should be split by the thumb 524 * 525 * @attr ref android.R.styleable#Switch_splitTrack 526 */ setSplitTrack(boolean splitTrack)527 public void setSplitTrack(boolean splitTrack) { 528 mSplitTrack = splitTrack; 529 invalidate(); 530 } 531 532 /** 533 * Returns whether the track should be split by the thumb. 534 * 535 * @attr ref android.R.styleable#Switch_splitTrack 536 */ getSplitTrack()537 public boolean getSplitTrack() { 538 return mSplitTrack; 539 } 540 541 /** 542 * Returns the text displayed when the button is in the checked state. 543 * 544 * @attr ref android.R.styleable#Switch_textOn 545 */ getTextOn()546 public CharSequence getTextOn() { 547 return mTextOn; 548 } 549 550 /** 551 * Sets the text displayed when the button is in the checked state. 552 * 553 * @attr ref android.R.styleable#Switch_textOn 554 */ setTextOn(CharSequence textOn)555 public void setTextOn(CharSequence textOn) { 556 mTextOn = textOn; 557 requestLayout(); 558 } 559 560 /** 561 * Returns the text displayed when the button is not in the checked state. 562 * 563 * @attr ref android.R.styleable#Switch_textOff 564 */ getTextOff()565 public CharSequence getTextOff() { 566 return mTextOff; 567 } 568 569 /** 570 * Sets the text displayed when the button is not in the checked state. 571 * 572 * @attr ref android.R.styleable#Switch_textOff 573 */ setTextOff(CharSequence textOff)574 public void setTextOff(CharSequence textOff) { 575 mTextOff = textOff; 576 requestLayout(); 577 } 578 579 /** 580 * Sets whether the on/off text should be displayed. 581 * 582 * @param showText {@code true} to display on/off text 583 * @attr ref android.R.styleable#Switch_showText 584 */ setShowText(boolean showText)585 public void setShowText(boolean showText) { 586 if (mShowText != showText) { 587 mShowText = showText; 588 requestLayout(); 589 } 590 } 591 592 /** 593 * @return whether the on/off text should be displayed 594 * @attr ref android.R.styleable#Switch_showText 595 */ getShowText()596 public boolean getShowText() { 597 return mShowText; 598 } 599 600 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)601 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 602 if (mShowText) { 603 if (mOnLayout == null) { 604 mOnLayout = makeLayout(mTextOn); 605 } 606 607 if (mOffLayout == null) { 608 mOffLayout = makeLayout(mTextOff); 609 } 610 } 611 612 final Rect padding = mTempRect; 613 final int thumbWidth; 614 final int thumbHeight; 615 if (mThumbDrawable != null) { 616 // Cached thumb width does not include padding. 617 mThumbDrawable.getPadding(padding); 618 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right; 619 thumbHeight = mThumbDrawable.getIntrinsicHeight(); 620 } else { 621 thumbWidth = 0; 622 thumbHeight = 0; 623 } 624 625 final int maxTextWidth; 626 if (mShowText) { 627 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()) 628 + mThumbTextPadding * 2; 629 } else { 630 maxTextWidth = 0; 631 } 632 633 mThumbWidth = Math.max(maxTextWidth, thumbWidth); 634 635 final int trackHeight; 636 if (mTrackDrawable != null) { 637 mTrackDrawable.getPadding(padding); 638 trackHeight = mTrackDrawable.getIntrinsicHeight(); 639 } else { 640 padding.setEmpty(); 641 trackHeight = 0; 642 } 643 644 // Adjust left and right padding to ensure there's enough room for the 645 // thumb's padding (when present). 646 int paddingLeft = padding.left; 647 int paddingRight = padding.right; 648 if (mThumbDrawable != null) { 649 final Insets inset = mThumbDrawable.getOpticalInsets(); 650 paddingLeft = Math.max(paddingLeft, inset.left); 651 paddingRight = Math.max(paddingRight, inset.right); 652 } 653 654 final int switchWidth = Math.max(mSwitchMinWidth, 655 2 * mThumbWidth + paddingLeft + paddingRight); 656 final int switchHeight = Math.max(trackHeight, thumbHeight); 657 mSwitchWidth = switchWidth; 658 mSwitchHeight = switchHeight; 659 660 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 661 662 final int measuredHeight = getMeasuredHeight(); 663 if (measuredHeight < switchHeight) { 664 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight); 665 } 666 } 667 668 @Override onPopulateAccessibilityEvent(AccessibilityEvent event)669 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 670 super.onPopulateAccessibilityEvent(event); 671 672 final CharSequence text = isChecked() ? mTextOn : mTextOff; 673 if (text != null) { 674 event.getText().add(text); 675 } 676 } 677 makeLayout(CharSequence text)678 private Layout makeLayout(CharSequence text) { 679 final CharSequence transformed = (mSwitchTransformationMethod != null) 680 ? mSwitchTransformationMethod.getTransformation(text, this) 681 : text; 682 683 return new StaticLayout(transformed, mTextPaint, 684 (int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)), 685 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true); 686 } 687 688 /** 689 * @return true if (x, y) is within the target area of the switch thumb 690 */ hitThumb(float x, float y)691 private boolean hitThumb(float x, float y) { 692 if (mThumbDrawable == null) { 693 return false; 694 } 695 696 // Relies on mTempRect, MUST be called first! 697 final int thumbOffset = getThumbOffset(); 698 699 mThumbDrawable.getPadding(mTempRect); 700 final int thumbTop = mSwitchTop - mTouchSlop; 701 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop; 702 final int thumbRight = thumbLeft + mThumbWidth + 703 mTempRect.left + mTempRect.right + mTouchSlop; 704 final int thumbBottom = mSwitchBottom + mTouchSlop; 705 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom; 706 } 707 708 @Override onTouchEvent(MotionEvent ev)709 public boolean onTouchEvent(MotionEvent ev) { 710 mVelocityTracker.addMovement(ev); 711 final int action = ev.getActionMasked(); 712 switch (action) { 713 case MotionEvent.ACTION_DOWN: { 714 final float x = ev.getX(); 715 final float y = ev.getY(); 716 if (isEnabled() && hitThumb(x, y)) { 717 mTouchMode = TOUCH_MODE_DOWN; 718 mTouchX = x; 719 mTouchY = y; 720 } 721 break; 722 } 723 724 case MotionEvent.ACTION_MOVE: { 725 switch (mTouchMode) { 726 case TOUCH_MODE_IDLE: 727 // Didn't target the thumb, treat normally. 728 break; 729 730 case TOUCH_MODE_DOWN: { 731 final float x = ev.getX(); 732 final float y = ev.getY(); 733 if (Math.abs(x - mTouchX) > mTouchSlop || 734 Math.abs(y - mTouchY) > mTouchSlop) { 735 mTouchMode = TOUCH_MODE_DRAGGING; 736 getParent().requestDisallowInterceptTouchEvent(true); 737 mTouchX = x; 738 mTouchY = y; 739 return true; 740 } 741 break; 742 } 743 744 case TOUCH_MODE_DRAGGING: { 745 final float x = ev.getX(); 746 final int thumbScrollRange = getThumbScrollRange(); 747 final float thumbScrollOffset = x - mTouchX; 748 float dPos; 749 if (thumbScrollRange != 0) { 750 dPos = thumbScrollOffset / thumbScrollRange; 751 } else { 752 // If the thumb scroll range is empty, just use the 753 // movement direction to snap on or off. 754 dPos = thumbScrollOffset > 0 ? 1 : -1; 755 } 756 if (isLayoutRtl()) { 757 dPos = -dPos; 758 } 759 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1); 760 if (newPos != mThumbPosition) { 761 mTouchX = x; 762 setThumbPosition(newPos); 763 } 764 return true; 765 } 766 } 767 break; 768 } 769 770 case MotionEvent.ACTION_UP: 771 case MotionEvent.ACTION_CANCEL: { 772 if (mTouchMode == TOUCH_MODE_DRAGGING) { 773 stopDrag(ev); 774 // Allow super class to handle pressed state, etc. 775 super.onTouchEvent(ev); 776 return true; 777 } 778 mTouchMode = TOUCH_MODE_IDLE; 779 mVelocityTracker.clear(); 780 break; 781 } 782 } 783 784 return super.onTouchEvent(ev); 785 } 786 cancelSuperTouch(MotionEvent ev)787 private void cancelSuperTouch(MotionEvent ev) { 788 MotionEvent cancel = MotionEvent.obtain(ev); 789 cancel.setAction(MotionEvent.ACTION_CANCEL); 790 super.onTouchEvent(cancel); 791 cancel.recycle(); 792 } 793 794 /** 795 * Called from onTouchEvent to end a drag operation. 796 * 797 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL 798 */ stopDrag(MotionEvent ev)799 private void stopDrag(MotionEvent ev) { 800 mTouchMode = TOUCH_MODE_IDLE; 801 802 // Commit the change if the event is up and not canceled and the switch 803 // has not been disabled during the drag. 804 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); 805 final boolean oldState = isChecked(); 806 final boolean newState; 807 if (commitChange) { 808 mVelocityTracker.computeCurrentVelocity(1000); 809 final float xvel = mVelocityTracker.getXVelocity(); 810 if (Math.abs(xvel) > mMinFlingVelocity) { 811 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0); 812 } else { 813 newState = getTargetCheckedState(); 814 } 815 } else { 816 newState = oldState; 817 } 818 819 if (newState != oldState) { 820 playSoundEffect(SoundEffectConstants.CLICK); 821 setChecked(newState); 822 } 823 824 cancelSuperTouch(ev); 825 } 826 animateThumbToCheckedState(boolean newCheckedState)827 private void animateThumbToCheckedState(boolean newCheckedState) { 828 final float targetPosition = newCheckedState ? 1 : 0; 829 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition); 830 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); 831 mPositionAnimator.setAutoCancel(true); 832 mPositionAnimator.start(); 833 } 834 cancelPositionAnimator()835 private void cancelPositionAnimator() { 836 if (mPositionAnimator != null) { 837 mPositionAnimator.cancel(); 838 } 839 } 840 getTargetCheckedState()841 private boolean getTargetCheckedState() { 842 return mThumbPosition > 0.5f; 843 } 844 845 /** 846 * Sets the thumb position as a decimal value between 0 (off) and 1 (on). 847 * 848 * @param position new position between [0,1] 849 */ setThumbPosition(float position)850 private void setThumbPosition(float position) { 851 mThumbPosition = position; 852 invalidate(); 853 } 854 855 @Override toggle()856 public void toggle() { 857 setChecked(!isChecked()); 858 } 859 860 @Override setChecked(boolean checked)861 public void setChecked(boolean checked) { 862 super.setChecked(checked); 863 864 // Calling the super method may result in setChecked() getting called 865 // recursively with a different value, so load the REAL value... 866 checked = isChecked(); 867 868 if (isAttachedToWindow() && isLaidOut()) { 869 animateThumbToCheckedState(checked); 870 } else { 871 // Immediately move the thumb to the new position. 872 cancelPositionAnimator(); 873 setThumbPosition(checked ? 1 : 0); 874 } 875 } 876 877 @Override onLayout(boolean changed, int left, int top, int right, int bottom)878 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 879 super.onLayout(changed, left, top, right, bottom); 880 881 int opticalInsetLeft = 0; 882 int opticalInsetRight = 0; 883 if (mThumbDrawable != null) { 884 final Rect trackPadding = mTempRect; 885 if (mTrackDrawable != null) { 886 mTrackDrawable.getPadding(trackPadding); 887 } else { 888 trackPadding.setEmpty(); 889 } 890 891 final Insets insets = mThumbDrawable.getOpticalInsets(); 892 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left); 893 opticalInsetRight = Math.max(0, insets.right - trackPadding.right); 894 } 895 896 final int switchRight; 897 final int switchLeft; 898 if (isLayoutRtl()) { 899 switchLeft = getPaddingLeft() + opticalInsetLeft; 900 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight; 901 } else { 902 switchRight = getWidth() - getPaddingRight() - opticalInsetRight; 903 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight; 904 } 905 906 final int switchTop; 907 final int switchBottom; 908 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { 909 default: 910 case Gravity.TOP: 911 switchTop = getPaddingTop(); 912 switchBottom = switchTop + mSwitchHeight; 913 break; 914 915 case Gravity.CENTER_VERTICAL: 916 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - 917 mSwitchHeight / 2; 918 switchBottom = switchTop + mSwitchHeight; 919 break; 920 921 case Gravity.BOTTOM: 922 switchBottom = getHeight() - getPaddingBottom(); 923 switchTop = switchBottom - mSwitchHeight; 924 break; 925 } 926 927 mSwitchLeft = switchLeft; 928 mSwitchTop = switchTop; 929 mSwitchBottom = switchBottom; 930 mSwitchRight = switchRight; 931 } 932 933 @Override draw(Canvas c)934 public void draw(Canvas c) { 935 final Rect padding = mTempRect; 936 final int switchLeft = mSwitchLeft; 937 final int switchTop = mSwitchTop; 938 final int switchRight = mSwitchRight; 939 final int switchBottom = mSwitchBottom; 940 941 int thumbInitialLeft = switchLeft + getThumbOffset(); 942 943 final Insets thumbInsets; 944 if (mThumbDrawable != null) { 945 thumbInsets = mThumbDrawable.getOpticalInsets(); 946 } else { 947 thumbInsets = Insets.NONE; 948 } 949 950 // Layout the track. 951 if (mTrackDrawable != null) { 952 mTrackDrawable.getPadding(padding); 953 954 // Adjust thumb position for track padding. 955 thumbInitialLeft += padding.left; 956 957 // If necessary, offset by the optical insets of the thumb asset. 958 int trackLeft = switchLeft; 959 int trackTop = switchTop; 960 int trackRight = switchRight; 961 int trackBottom = switchBottom; 962 if (thumbInsets != Insets.NONE) { 963 if (thumbInsets.left > padding.left) { 964 trackLeft += thumbInsets.left - padding.left; 965 } 966 if (thumbInsets.top > padding.top) { 967 trackTop += thumbInsets.top - padding.top; 968 } 969 if (thumbInsets.right > padding.right) { 970 trackRight -= thumbInsets.right - padding.right; 971 } 972 if (thumbInsets.bottom > padding.bottom) { 973 trackBottom -= thumbInsets.bottom - padding.bottom; 974 } 975 } 976 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom); 977 } 978 979 // Layout the thumb. 980 if (mThumbDrawable != null) { 981 mThumbDrawable.getPadding(padding); 982 983 final int thumbLeft = thumbInitialLeft - padding.left; 984 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right; 985 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); 986 987 final Drawable background = getBackground(); 988 if (background != null) { 989 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom); 990 } 991 } 992 993 // Draw the background. 994 super.draw(c); 995 } 996 997 @Override onDraw(Canvas canvas)998 protected void onDraw(Canvas canvas) { 999 super.onDraw(canvas); 1000 1001 final Rect padding = mTempRect; 1002 final Drawable trackDrawable = mTrackDrawable; 1003 if (trackDrawable != null) { 1004 trackDrawable.getPadding(padding); 1005 } else { 1006 padding.setEmpty(); 1007 } 1008 1009 final int switchTop = mSwitchTop; 1010 final int switchBottom = mSwitchBottom; 1011 final int switchInnerTop = switchTop + padding.top; 1012 final int switchInnerBottom = switchBottom - padding.bottom; 1013 1014 final Drawable thumbDrawable = mThumbDrawable; 1015 if (trackDrawable != null) { 1016 if (mSplitTrack && thumbDrawable != null) { 1017 final Insets insets = thumbDrawable.getOpticalInsets(); 1018 thumbDrawable.copyBounds(padding); 1019 padding.left += insets.left; 1020 padding.right -= insets.right; 1021 1022 final int saveCount = canvas.save(); 1023 canvas.clipRect(padding, Op.DIFFERENCE); 1024 trackDrawable.draw(canvas); 1025 canvas.restoreToCount(saveCount); 1026 } else { 1027 trackDrawable.draw(canvas); 1028 } 1029 } 1030 1031 final int saveCount = canvas.save(); 1032 1033 if (thumbDrawable != null) { 1034 thumbDrawable.draw(canvas); 1035 } 1036 1037 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; 1038 if (switchText != null) { 1039 final int drawableState[] = getDrawableState(); 1040 if (mTextColors != null) { 1041 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); 1042 } 1043 mTextPaint.drawableState = drawableState; 1044 1045 final int cX; 1046 if (thumbDrawable != null) { 1047 final Rect bounds = thumbDrawable.getBounds(); 1048 cX = bounds.left + bounds.right; 1049 } else { 1050 cX = getWidth(); 1051 } 1052 1053 final int left = cX / 2 - switchText.getWidth() / 2; 1054 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; 1055 canvas.translate(left, top); 1056 switchText.draw(canvas); 1057 } 1058 1059 canvas.restoreToCount(saveCount); 1060 } 1061 1062 @Override getCompoundPaddingLeft()1063 public int getCompoundPaddingLeft() { 1064 if (!isLayoutRtl()) { 1065 return super.getCompoundPaddingLeft(); 1066 } 1067 int padding = super.getCompoundPaddingLeft() + mSwitchWidth; 1068 if (!TextUtils.isEmpty(getText())) { 1069 padding += mSwitchPadding; 1070 } 1071 return padding; 1072 } 1073 1074 @Override getCompoundPaddingRight()1075 public int getCompoundPaddingRight() { 1076 if (isLayoutRtl()) { 1077 return super.getCompoundPaddingRight(); 1078 } 1079 int padding = super.getCompoundPaddingRight() + mSwitchWidth; 1080 if (!TextUtils.isEmpty(getText())) { 1081 padding += mSwitchPadding; 1082 } 1083 return padding; 1084 } 1085 1086 /** 1087 * Translates thumb position to offset according to current RTL setting and 1088 * thumb scroll range. Accounts for both track and thumb padding. 1089 * 1090 * @return thumb offset 1091 */ getThumbOffset()1092 private int getThumbOffset() { 1093 final float thumbPosition; 1094 if (isLayoutRtl()) { 1095 thumbPosition = 1 - mThumbPosition; 1096 } else { 1097 thumbPosition = mThumbPosition; 1098 } 1099 return (int) (thumbPosition * getThumbScrollRange() + 0.5f); 1100 } 1101 getThumbScrollRange()1102 private int getThumbScrollRange() { 1103 if (mTrackDrawable != null) { 1104 final Rect padding = mTempRect; 1105 mTrackDrawable.getPadding(padding); 1106 1107 final Insets insets; 1108 if (mThumbDrawable != null) { 1109 insets = mThumbDrawable.getOpticalInsets(); 1110 } else { 1111 insets = Insets.NONE; 1112 } 1113 1114 return mSwitchWidth - mThumbWidth - padding.left - padding.right 1115 - insets.left - insets.right; 1116 } else { 1117 return 0; 1118 } 1119 } 1120 1121 @Override onCreateDrawableState(int extraSpace)1122 protected int[] onCreateDrawableState(int extraSpace) { 1123 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 1124 if (isChecked()) { 1125 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 1126 } 1127 return drawableState; 1128 } 1129 1130 @Override drawableStateChanged()1131 protected void drawableStateChanged() { 1132 super.drawableStateChanged(); 1133 1134 final int[] myDrawableState = getDrawableState(); 1135 1136 if (mThumbDrawable != null) { 1137 mThumbDrawable.setState(myDrawableState); 1138 } 1139 1140 if (mTrackDrawable != null) { 1141 mTrackDrawable.setState(myDrawableState); 1142 } 1143 1144 invalidate(); 1145 } 1146 1147 @Override drawableHotspotChanged(float x, float y)1148 public void drawableHotspotChanged(float x, float y) { 1149 super.drawableHotspotChanged(x, y); 1150 1151 if (mThumbDrawable != null) { 1152 mThumbDrawable.setHotspot(x, y); 1153 } 1154 1155 if (mTrackDrawable != null) { 1156 mTrackDrawable.setHotspot(x, y); 1157 } 1158 } 1159 1160 @Override verifyDrawable(Drawable who)1161 protected boolean verifyDrawable(Drawable who) { 1162 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; 1163 } 1164 1165 @Override jumpDrawablesToCurrentState()1166 public void jumpDrawablesToCurrentState() { 1167 super.jumpDrawablesToCurrentState(); 1168 1169 if (mThumbDrawable != null) { 1170 mThumbDrawable.jumpToCurrentState(); 1171 } 1172 1173 if (mTrackDrawable != null) { 1174 mTrackDrawable.jumpToCurrentState(); 1175 } 1176 1177 if (mPositionAnimator != null && mPositionAnimator.isRunning()) { 1178 mPositionAnimator.end(); 1179 mPositionAnimator = null; 1180 } 1181 } 1182 1183 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1184 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1185 super.onInitializeAccessibilityEvent(event); 1186 event.setClassName(Switch.class.getName()); 1187 } 1188 1189 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1190 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1191 super.onInitializeAccessibilityNodeInfo(info); 1192 info.setClassName(Switch.class.getName()); 1193 CharSequence switchText = isChecked() ? mTextOn : mTextOff; 1194 if (!TextUtils.isEmpty(switchText)) { 1195 CharSequence oldText = info.getText(); 1196 if (TextUtils.isEmpty(oldText)) { 1197 info.setText(switchText); 1198 } else { 1199 StringBuilder newText = new StringBuilder(); 1200 newText.append(oldText).append(' ').append(switchText); 1201 info.setText(newText); 1202 } 1203 } 1204 } 1205 1206 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") { 1207 @Override 1208 public Float get(Switch object) { 1209 return object.mThumbPosition; 1210 } 1211 1212 @Override 1213 public void setValue(Switch object, float value) { 1214 object.setThumbPosition(value); 1215 } 1216 }; 1217 } 1218