1 /* 2 * Copyright (C) 2007 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.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.UnsupportedAppUsage; 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.content.res.TypedArray; 25 import android.graphics.BlendMode; 26 import android.graphics.Canvas; 27 import android.graphics.Insets; 28 import android.graphics.PorterDuff; 29 import android.graphics.Rect; 30 import android.graphics.Region.Op; 31 import android.graphics.drawable.Drawable; 32 import android.os.Bundle; 33 import android.util.AttributeSet; 34 import android.view.KeyEvent; 35 import android.view.MotionEvent; 36 import android.view.ViewConfiguration; 37 import android.view.accessibility.AccessibilityNodeInfo; 38 import android.view.inspector.InspectableProperty; 39 40 import com.android.internal.R; 41 import com.android.internal.util.Preconditions; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.List; 46 47 48 /** 49 * AbsSeekBar extends the capabilities of ProgressBar by adding a draggable thumb. 50 */ 51 public abstract class AbsSeekBar extends ProgressBar { 52 private final Rect mTempRect = new Rect(); 53 54 @UnsupportedAppUsage 55 private Drawable mThumb; 56 private ColorStateList mThumbTintList = null; 57 private BlendMode mThumbBlendMode = null; 58 private boolean mHasThumbTint = false; 59 private boolean mHasThumbBlendMode = false; 60 61 private Drawable mTickMark; 62 private ColorStateList mTickMarkTintList = null; 63 private BlendMode mTickMarkBlendMode = null; 64 private boolean mHasTickMarkTint = false; 65 private boolean mHasTickMarkBlendMode = false; 66 67 private int mThumbOffset; 68 @UnsupportedAppUsage 69 private boolean mSplitTrack; 70 71 /** 72 * On touch, this offset plus the scaled value from the position of the 73 * touch will form the progress value. Usually 0. 74 */ 75 @UnsupportedAppUsage 76 float mTouchProgressOffset; 77 78 /** 79 * Whether this is user seekable. 80 */ 81 @UnsupportedAppUsage 82 boolean mIsUserSeekable = true; 83 84 /** 85 * On key presses (right or left), the amount to increment/decrement the 86 * progress. 87 */ 88 private int mKeyProgressIncrement = 1; 89 90 private static final int NO_ALPHA = 0xFF; 91 @UnsupportedAppUsage 92 private float mDisabledAlpha; 93 94 private int mScaledTouchSlop; 95 private float mTouchDownX; 96 @UnsupportedAppUsage 97 private boolean mIsDragging; 98 99 private List<Rect> mUserGestureExclusionRects = Collections.emptyList(); 100 private final List<Rect> mGestureExclusionRects = new ArrayList<>(); 101 private final Rect mThumbRect = new Rect(); 102 103 public AbsSeekBar(Context context) { 104 super(context); 105 } 106 107 public AbsSeekBar(Context context, AttributeSet attrs) { 108 super(context, attrs); 109 } 110 111 public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { 112 this(context, attrs, defStyleAttr, 0); 113 } 114 115 public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 116 super(context, attrs, defStyleAttr, defStyleRes); 117 118 final TypedArray a = context.obtainStyledAttributes( 119 attrs, R.styleable.SeekBar, defStyleAttr, defStyleRes); 120 saveAttributeDataForStyleable(context, R.styleable.SeekBar, attrs, a, defStyleAttr, 121 defStyleRes); 122 123 final Drawable thumb = a.getDrawable(R.styleable.SeekBar_thumb); 124 setThumb(thumb); 125 126 if (a.hasValue(R.styleable.SeekBar_thumbTintMode)) { 127 mThumbBlendMode = Drawable.parseBlendMode(a.getInt( 128 R.styleable.SeekBar_thumbTintMode, -1), mThumbBlendMode); 129 mHasThumbBlendMode = true; 130 } 131 132 if (a.hasValue(R.styleable.SeekBar_thumbTint)) { 133 mThumbTintList = a.getColorStateList(R.styleable.SeekBar_thumbTint); 134 mHasThumbTint = true; 135 } 136 137 final Drawable tickMark = a.getDrawable(R.styleable.SeekBar_tickMark); 138 setTickMark(tickMark); 139 140 if (a.hasValue(R.styleable.SeekBar_tickMarkTintMode)) { 141 mTickMarkBlendMode = Drawable.parseBlendMode(a.getInt( 142 R.styleable.SeekBar_tickMarkTintMode, -1), mTickMarkBlendMode); 143 mHasTickMarkBlendMode = true; 144 } 145 146 if (a.hasValue(R.styleable.SeekBar_tickMarkTint)) { 147 mTickMarkTintList = a.getColorStateList(R.styleable.SeekBar_tickMarkTint); 148 mHasTickMarkTint = true; 149 } 150 151 mSplitTrack = a.getBoolean(R.styleable.SeekBar_splitTrack, false); 152 153 // Guess thumb offset if thumb != null, but allow layout to override. 154 final int thumbOffset = a.getDimensionPixelOffset( 155 R.styleable.SeekBar_thumbOffset, getThumbOffset()); 156 setThumbOffset(thumbOffset); 157 158 final boolean useDisabledAlpha = a.getBoolean(R.styleable.SeekBar_useDisabledAlpha, true); 159 a.recycle(); 160 161 if (useDisabledAlpha) { 162 final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Theme, 0, 0); 163 mDisabledAlpha = ta.getFloat(R.styleable.Theme_disabledAlpha, 0.5f); 164 ta.recycle(); 165 } else { 166 mDisabledAlpha = 1.0f; 167 } 168 169 applyThumbTint(); 170 applyTickMarkTint(); 171 172 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 173 } 174 175 /** 176 * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar. 177 * <p> 178 * If the thumb is a valid drawable (i.e. not null), half its width will be 179 * used as the new thumb offset (@see #setThumbOffset(int)). 180 * 181 * @param thumb Drawable representing the thumb 182 */ 183 public void setThumb(Drawable thumb) { 184 final boolean needUpdate; 185 // This way, calling setThumb again with the same bitmap will result in 186 // it recalcuating mThumbOffset (if for example it the bounds of the 187 // drawable changed) 188 if (mThumb != null && thumb != mThumb) { 189 mThumb.setCallback(null); 190 needUpdate = true; 191 } else { 192 needUpdate = false; 193 } 194 195 if (thumb != null) { 196 thumb.setCallback(this); 197 if (canResolveLayoutDirection()) { 198 thumb.setLayoutDirection(getLayoutDirection()); 199 } 200 201 // Assuming the thumb drawable is symmetric, set the thumb offset 202 // such that the thumb will hang halfway off either edge of the 203 // progress bar. 204 mThumbOffset = thumb.getIntrinsicWidth() / 2; 205 206 // If we're updating get the new states 207 if (needUpdate && 208 (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth() 209 || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) { 210 requestLayout(); 211 } 212 } 213 214 mThumb = thumb; 215 216 applyThumbTint(); 217 invalidate(); 218 219 if (needUpdate) { 220 updateThumbAndTrackPos(getWidth(), getHeight()); 221 if (thumb != null && thumb.isStateful()) { 222 // Note that if the states are different this won't work. 223 // For now, let's consider that an app bug. 224 int[] state = getDrawableState(); 225 thumb.setState(state); 226 } 227 } 228 } 229 230 /** 231 * Return the drawable used to represent the scroll thumb - the component that 232 * the user can drag back and forth indicating the current value by its position. 233 * 234 * @return The current thumb drawable 235 */ 236 public Drawable getThumb() { 237 return mThumb; 238 } 239 240 /** 241 * Applies a tint to the thumb drawable. Does not modify the current tint 242 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 243 * <p> 244 * Subsequent calls to {@link #setThumb(Drawable)} will automatically 245 * mutate the drawable and apply the specified tint and tint mode using 246 * {@link Drawable#setTintList(ColorStateList)}. 247 * 248 * @param tint the tint to apply, may be {@code null} to clear tint 249 * 250 * @attr ref android.R.styleable#SeekBar_thumbTint 251 * @see #getThumbTintList() 252 * @see Drawable#setTintList(ColorStateList) 253 */ 254 public void setThumbTintList(@Nullable ColorStateList tint) { 255 mThumbTintList = tint; 256 mHasThumbTint = true; 257 258 applyThumbTint(); 259 } 260 261 /** 262 * Returns the tint applied to the thumb drawable, if specified. 263 * 264 * @return the tint applied to the thumb drawable 265 * @attr ref android.R.styleable#SeekBar_thumbTint 266 * @see #setThumbTintList(ColorStateList) 267 */ 268 @InspectableProperty(name = "thumbTint") 269 @Nullable 270 public ColorStateList getThumbTintList() { 271 return mThumbTintList; 272 } 273 274 /** 275 * Specifies the blending mode used to apply the tint specified by 276 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. The 277 * default mode is {@link PorterDuff.Mode#SRC_IN}. 278 * 279 * @param tintMode the blending mode used to apply the tint, may be 280 * {@code null} to clear tint 281 * 282 * @attr ref android.R.styleable#SeekBar_thumbTintMode 283 * @see #getThumbTintMode() 284 * @see Drawable#setTintMode(PorterDuff.Mode) 285 */ 286 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { 287 setThumbTintBlendMode(tintMode != null ? BlendMode.fromValue(tintMode.nativeInt) : 288 null); 289 } 290 291 /** 292 * Specifies the blending mode used to apply the tint specified by 293 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. The 294 * default mode is {@link BlendMode#SRC_IN}. 295 * 296 * @param blendMode the blending mode used to apply the tint, may be 297 * {@code null} to clear tint 298 * 299 * @attr ref android.R.styleable#SeekBar_thumbTintMode 300 * @see #getThumbTintMode() 301 * @see Drawable#setTintBlendMode(BlendMode) 302 */ 303 public void setThumbTintBlendMode(@Nullable BlendMode blendMode) { 304 mThumbBlendMode = blendMode; 305 mHasThumbBlendMode = true; 306 applyThumbTint(); 307 } 308 309 /** 310 * Returns the blending mode used to apply the tint to the thumb drawable, 311 * if specified. 312 * 313 * @return the blending mode used to apply the tint to the thumb drawable 314 * @attr ref android.R.styleable#SeekBar_thumbTintMode 315 * @see #setThumbTintMode(PorterDuff.Mode) 316 */ 317 @InspectableProperty 318 @Nullable 319 public PorterDuff.Mode getThumbTintMode() { 320 return mThumbBlendMode != null 321 ? BlendMode.blendModeToPorterDuffMode(mThumbBlendMode) : null; 322 } 323 324 /** 325 * Returns the blending mode used to apply the tint to the thumb drawable, 326 * if specified. 327 * 328 * @return the blending mode used to apply the tint to the thumb drawable 329 * @attr ref android.R.styleable#SeekBar_thumbTintMode 330 * @see #setThumbTintBlendMode(BlendMode) 331 */ 332 @Nullable 333 public BlendMode getThumbTintBlendMode() { 334 return mThumbBlendMode; 335 } 336 337 private void applyThumbTint() { 338 if (mThumb != null && (mHasThumbTint || mHasThumbBlendMode)) { 339 mThumb = mThumb.mutate(); 340 341 if (mHasThumbTint) { 342 mThumb.setTintList(mThumbTintList); 343 } 344 345 if (mHasThumbBlendMode) { 346 mThumb.setTintBlendMode(mThumbBlendMode); 347 } 348 349 // The drawable (or one of its children) may not have been 350 // stateful before applying the tint, so let's try again. 351 if (mThumb.isStateful()) { 352 mThumb.setState(getDrawableState()); 353 } 354 } 355 } 356 357 /** 358 * @see #setThumbOffset(int) 359 */ 360 public int getThumbOffset() { 361 return mThumbOffset; 362 } 363 364 /** 365 * Sets the thumb offset that allows the thumb to extend out of the range of 366 * the track. 367 * 368 * @param thumbOffset The offset amount in pixels. 369 */ 370 public void setThumbOffset(int thumbOffset) { 371 mThumbOffset = thumbOffset; 372 invalidate(); 373 } 374 375 /** 376 * Specifies whether the track should be split by the thumb. When true, 377 * the thumb's optical bounds will be clipped out of the track drawable, 378 * then the thumb will be drawn into the resulting gap. 379 * 380 * @param splitTrack Whether the track should be split by the thumb 381 */ 382 public void setSplitTrack(boolean splitTrack) { 383 mSplitTrack = splitTrack; 384 invalidate(); 385 } 386 387 /** 388 * Returns whether the track should be split by the thumb. 389 */ 390 public boolean getSplitTrack() { 391 return mSplitTrack; 392 } 393 394 /** 395 * Sets the drawable displayed at each progress position, e.g. at each 396 * possible thumb position. 397 * 398 * @param tickMark the drawable to display at each progress position 399 */ 400 public void setTickMark(Drawable tickMark) { 401 if (mTickMark != null) { 402 mTickMark.setCallback(null); 403 } 404 405 mTickMark = tickMark; 406 407 if (tickMark != null) { 408 tickMark.setCallback(this); 409 tickMark.setLayoutDirection(getLayoutDirection()); 410 if (tickMark.isStateful()) { 411 tickMark.setState(getDrawableState()); 412 } 413 applyTickMarkTint(); 414 } 415 416 invalidate(); 417 } 418 419 /** 420 * @return the drawable displayed at each progress position 421 */ 422 public Drawable getTickMark() { 423 return mTickMark; 424 } 425 426 /** 427 * Applies a tint to the tick mark drawable. Does not modify the current tint 428 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 429 * <p> 430 * Subsequent calls to {@link #setTickMark(Drawable)} will automatically 431 * mutate the drawable and apply the specified tint and tint mode using 432 * {@link Drawable#setTintList(ColorStateList)}. 433 * 434 * @param tint the tint to apply, may be {@code null} to clear tint 435 * 436 * @attr ref android.R.styleable#SeekBar_tickMarkTint 437 * @see #getTickMarkTintList() 438 * @see Drawable#setTintList(ColorStateList) 439 */ 440 public void setTickMarkTintList(@Nullable ColorStateList tint) { 441 mTickMarkTintList = tint; 442 mHasTickMarkTint = true; 443 444 applyTickMarkTint(); 445 } 446 447 /** 448 * Returns the tint applied to the tick mark drawable, if specified. 449 * 450 * @return the tint applied to the tick mark drawable 451 * @attr ref android.R.styleable#SeekBar_tickMarkTint 452 * @see #setTickMarkTintList(ColorStateList) 453 */ 454 @InspectableProperty(name = "tickMarkTint") 455 @Nullable 456 public ColorStateList getTickMarkTintList() { 457 return mTickMarkTintList; 458 } 459 460 /** 461 * Specifies the blending mode used to apply the tint specified by 462 * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The 463 * default mode is {@link PorterDuff.Mode#SRC_IN}. 464 * 465 * @param tintMode the blending mode used to apply the tint, may be 466 * {@code null} to clear tint 467 * 468 * @attr ref android.R.styleable#SeekBar_tickMarkTintMode 469 * @see #getTickMarkTintMode() 470 * @see Drawable#setTintMode(PorterDuff.Mode) 471 */ 472 public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) { 473 setTickMarkTintBlendMode(tintMode != null ? BlendMode.fromValue(tintMode.nativeInt) : null); 474 } 475 476 /** 477 * Specifies the blending mode used to apply the tint specified by 478 * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The 479 * default mode is {@link BlendMode#SRC_IN}. 480 * 481 * @param blendMode the blending mode used to apply the tint, may be 482 * {@code null} to clear tint 483 * 484 * @attr ref android.R.styleable#SeekBar_tickMarkTintMode 485 * @see #getTickMarkTintMode() 486 * @see Drawable#setTintBlendMode(BlendMode) 487 */ 488 public void setTickMarkTintBlendMode(@Nullable BlendMode blendMode) { 489 mTickMarkBlendMode = blendMode; 490 mHasTickMarkBlendMode = true; 491 492 applyTickMarkTint(); 493 } 494 495 /** 496 * Returns the blending mode used to apply the tint to the tick mark drawable, 497 * if specified. 498 * 499 * @return the blending mode used to apply the tint to the tick mark drawable 500 * @attr ref android.R.styleable#SeekBar_tickMarkTintMode 501 * @see #setTickMarkTintMode(PorterDuff.Mode) 502 */ 503 @InspectableProperty 504 @Nullable 505 public PorterDuff.Mode getTickMarkTintMode() { 506 return mTickMarkBlendMode != null 507 ? BlendMode.blendModeToPorterDuffMode(mTickMarkBlendMode) : null; 508 } 509 510 /** 511 * Returns the blending mode used to apply the tint to the tick mark drawable, 512 * if specified. 513 * 514 * @return the blending mode used to apply the tint to the tick mark drawable 515 * @attr ref android.R.styleable#SeekBar_tickMarkTintMode 516 * @see #setTickMarkTintMode(PorterDuff.Mode) 517 */ 518 @InspectableProperty(attributeId = android.R.styleable.SeekBar_tickMarkTintMode) 519 @Nullable 520 public BlendMode getTickMarkTintBlendMode() { 521 return mTickMarkBlendMode; 522 } 523 524 private void applyTickMarkTint() { 525 if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkBlendMode)) { 526 mTickMark = mTickMark.mutate(); 527 528 if (mHasTickMarkTint) { 529 mTickMark.setTintList(mTickMarkTintList); 530 } 531 532 if (mHasTickMarkBlendMode) { 533 mTickMark.setTintBlendMode(mTickMarkBlendMode); 534 } 535 536 // The drawable (or one of its children) may not have been 537 // stateful before applying the tint, so let's try again. 538 if (mTickMark.isStateful()) { 539 mTickMark.setState(getDrawableState()); 540 } 541 } 542 } 543 544 /** 545 * Sets the amount of progress changed via the arrow keys. 546 * 547 * @param increment The amount to increment or decrement when the user 548 * presses the arrow keys. 549 */ 550 public void setKeyProgressIncrement(int increment) { 551 mKeyProgressIncrement = increment < 0 ? -increment : increment; 552 } 553 554 /** 555 * Returns the amount of progress changed via the arrow keys. 556 * <p> 557 * By default, this will be a value that is derived from the progress range. 558 * 559 * @return The amount to increment or decrement when the user presses the 560 * arrow keys. This will be positive. 561 */ 562 public int getKeyProgressIncrement() { 563 return mKeyProgressIncrement; 564 } 565 566 @Override 567 public synchronized void setMin(int min) { 568 super.setMin(min); 569 int range = getMax() - getMin(); 570 571 if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) { 572 573 // It will take the user too long to change this via keys, change it 574 // to something more reasonable 575 setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20))); 576 } 577 } 578 579 @Override 580 public synchronized void setMax(int max) { 581 super.setMax(max); 582 int range = getMax() - getMin(); 583 584 if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) { 585 // It will take the user too long to change this via keys, change it 586 // to something more reasonable 587 setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20))); 588 } 589 } 590 591 @Override 592 protected boolean verifyDrawable(@NonNull Drawable who) { 593 return who == mThumb || who == mTickMark || super.verifyDrawable(who); 594 } 595 596 @Override 597 public void jumpDrawablesToCurrentState() { 598 super.jumpDrawablesToCurrentState(); 599 600 if (mThumb != null) { 601 mThumb.jumpToCurrentState(); 602 } 603 604 if (mTickMark != null) { 605 mTickMark.jumpToCurrentState(); 606 } 607 } 608 609 @Override 610 protected void drawableStateChanged() { 611 super.drawableStateChanged(); 612 613 final Drawable progressDrawable = getProgressDrawable(); 614 if (progressDrawable != null && mDisabledAlpha < 1.0f) { 615 progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); 616 } 617 618 final Drawable thumb = mThumb; 619 if (thumb != null && thumb.isStateful() 620 && thumb.setState(getDrawableState())) { 621 invalidateDrawable(thumb); 622 } 623 624 final Drawable tickMark = mTickMark; 625 if (tickMark != null && tickMark.isStateful() 626 && tickMark.setState(getDrawableState())) { 627 invalidateDrawable(tickMark); 628 } 629 } 630 631 @Override 632 public void drawableHotspotChanged(float x, float y) { 633 super.drawableHotspotChanged(x, y); 634 635 if (mThumb != null) { 636 mThumb.setHotspot(x, y); 637 } 638 } 639 640 @Override 641 void onVisualProgressChanged(int id, float scale) { 642 super.onVisualProgressChanged(id, scale); 643 644 if (id == R.id.progress) { 645 final Drawable thumb = mThumb; 646 if (thumb != null) { 647 setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); 648 649 // Since we draw translated, the drawable's bounds that it signals 650 // for invalidation won't be the actual bounds we want invalidated, 651 // so just invalidate this whole view. 652 invalidate(); 653 } 654 } 655 } 656 657 @Override 658 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 659 super.onSizeChanged(w, h, oldw, oldh); 660 661 updateThumbAndTrackPos(w, h); 662 } 663 664 private void updateThumbAndTrackPos(int w, int h) { 665 final int paddedHeight = h - mPaddingTop - mPaddingBottom; 666 final Drawable track = getCurrentDrawable(); 667 final Drawable thumb = mThumb; 668 669 // The max height does not incorporate padding, whereas the height 670 // parameter does. 671 final int trackHeight = Math.min(mMaxHeight, paddedHeight); 672 final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); 673 674 // Apply offset to whichever item is taller. 675 final int trackOffset; 676 final int thumbOffset; 677 if (thumbHeight > trackHeight) { 678 final int offsetHeight = (paddedHeight - thumbHeight) / 2; 679 trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2; 680 thumbOffset = offsetHeight; 681 } else { 682 final int offsetHeight = (paddedHeight - trackHeight) / 2; 683 trackOffset = offsetHeight; 684 thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2; 685 } 686 687 if (track != null) { 688 final int trackWidth = w - mPaddingRight - mPaddingLeft; 689 track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight); 690 } 691 692 if (thumb != null) { 693 setThumbPos(w, thumb, getScale(), thumbOffset); 694 } 695 } 696 697 private float getScale() { 698 int min = getMin(); 699 int max = getMax(); 700 int range = max - min; 701 return range > 0 ? (getProgress() - min) / (float) range : 0; 702 } 703 704 /** 705 * Updates the thumb drawable bounds. 706 * 707 * @param w Width of the view, including padding 708 * @param thumb Drawable used for the thumb 709 * @param scale Current progress between 0 and 1 710 * @param offset Vertical offset for centering. If set to 711 * {@link Integer#MIN_VALUE}, the current offset will be used. 712 */ 713 private void setThumbPos(int w, Drawable thumb, float scale, int offset) { 714 int available = w - mPaddingLeft - mPaddingRight; 715 final int thumbWidth = thumb.getIntrinsicWidth(); 716 final int thumbHeight = thumb.getIntrinsicHeight(); 717 available -= thumbWidth; 718 719 // The extra space for the thumb to move on the track 720 available += mThumbOffset * 2; 721 722 final int thumbPos = (int) (scale * available + 0.5f); 723 724 final int top, bottom; 725 if (offset == Integer.MIN_VALUE) { 726 final Rect oldBounds = thumb.getBounds(); 727 top = oldBounds.top; 728 bottom = oldBounds.bottom; 729 } else { 730 top = offset; 731 bottom = offset + thumbHeight; 732 } 733 734 final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos; 735 final int right = left + thumbWidth; 736 737 final Drawable background = getBackground(); 738 if (background != null) { 739 final int offsetX = mPaddingLeft - mThumbOffset; 740 final int offsetY = mPaddingTop; 741 background.setHotspotBounds(left + offsetX, top + offsetY, 742 right + offsetX, bottom + offsetY); 743 } 744 745 // Canvas will be translated, so 0,0 is where we start drawing 746 thumb.setBounds(left, top, right, bottom); 747 updateGestureExclusionRects(); 748 } 749 750 @Override 751 public void setSystemGestureExclusionRects(@NonNull List<Rect> rects) { 752 Preconditions.checkNotNull(rects, "rects must not be null"); 753 mUserGestureExclusionRects = rects; 754 updateGestureExclusionRects(); 755 } 756 757 private void updateGestureExclusionRects() { 758 final Drawable thumb = mThumb; 759 if (thumb == null) { 760 super.setSystemGestureExclusionRects(mUserGestureExclusionRects); 761 return; 762 } 763 mGestureExclusionRects.clear(); 764 thumb.copyBounds(mThumbRect); 765 mGestureExclusionRects.add(mThumbRect); 766 mGestureExclusionRects.addAll(mUserGestureExclusionRects); 767 super.setSystemGestureExclusionRects(mGestureExclusionRects); 768 } 769 770 /** 771 * @hide 772 */ 773 @Override 774 public void onResolveDrawables(int layoutDirection) { 775 super.onResolveDrawables(layoutDirection); 776 777 if (mThumb != null) { 778 mThumb.setLayoutDirection(layoutDirection); 779 } 780 } 781 782 @Override 783 protected synchronized void onDraw(Canvas canvas) { 784 super.onDraw(canvas); 785 drawThumb(canvas); 786 } 787 788 @Override 789 void drawTrack(Canvas canvas) { 790 final Drawable thumbDrawable = mThumb; 791 if (thumbDrawable != null && mSplitTrack) { 792 final Insets insets = thumbDrawable.getOpticalInsets(); 793 final Rect tempRect = mTempRect; 794 thumbDrawable.copyBounds(tempRect); 795 tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop); 796 tempRect.left += insets.left; 797 tempRect.right -= insets.right; 798 799 final int saveCount = canvas.save(); 800 canvas.clipRect(tempRect, Op.DIFFERENCE); 801 super.drawTrack(canvas); 802 drawTickMarks(canvas); 803 canvas.restoreToCount(saveCount); 804 } else { 805 super.drawTrack(canvas); 806 drawTickMarks(canvas); 807 } 808 } 809 810 /** 811 * @hide 812 */ 813 protected void drawTickMarks(Canvas canvas) { 814 if (mTickMark != null) { 815 final int count = getMax() - getMin(); 816 if (count > 1) { 817 final int w = mTickMark.getIntrinsicWidth(); 818 final int h = mTickMark.getIntrinsicHeight(); 819 final int halfW = w >= 0 ? w / 2 : 1; 820 final int halfH = h >= 0 ? h / 2 : 1; 821 mTickMark.setBounds(-halfW, -halfH, halfW, halfH); 822 823 final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count; 824 final int saveCount = canvas.save(); 825 canvas.translate(mPaddingLeft, getHeight() / 2); 826 for (int i = 0; i <= count; i++) { 827 mTickMark.draw(canvas); 828 canvas.translate(spacing, 0); 829 } 830 canvas.restoreToCount(saveCount); 831 } 832 } 833 } 834 835 /** 836 * Draw the thumb. 837 */ 838 @UnsupportedAppUsage 839 void drawThumb(Canvas canvas) { 840 if (mThumb != null) { 841 final int saveCount = canvas.save(); 842 // Translate the padding. For the x, we need to allow the thumb to 843 // draw in its extra space 844 canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop); 845 mThumb.draw(canvas); 846 canvas.restoreToCount(saveCount); 847 } 848 } 849 850 @Override 851 protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 852 Drawable d = getCurrentDrawable(); 853 854 int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight(); 855 int dw = 0; 856 int dh = 0; 857 if (d != null) { 858 dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); 859 dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); 860 dh = Math.max(thumbHeight, dh); 861 } 862 dw += mPaddingLeft + mPaddingRight; 863 dh += mPaddingTop + mPaddingBottom; 864 865 setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), 866 resolveSizeAndState(dh, heightMeasureSpec, 0)); 867 } 868 869 @Override 870 public boolean onTouchEvent(MotionEvent event) { 871 if (!mIsUserSeekable || !isEnabled()) { 872 return false; 873 } 874 875 switch (event.getAction()) { 876 case MotionEvent.ACTION_DOWN: 877 if (isInScrollingContainer()) { 878 mTouchDownX = event.getX(); 879 } else { 880 startDrag(event); 881 } 882 break; 883 884 case MotionEvent.ACTION_MOVE: 885 if (mIsDragging) { 886 trackTouchEvent(event); 887 } else { 888 final float x = event.getX(); 889 if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { 890 startDrag(event); 891 } 892 } 893 break; 894 895 case MotionEvent.ACTION_UP: 896 if (mIsDragging) { 897 trackTouchEvent(event); 898 onStopTrackingTouch(); 899 setPressed(false); 900 } else { 901 // Touch up when we never crossed the touch slop threshold should 902 // be interpreted as a tap-seek to that location. 903 onStartTrackingTouch(); 904 trackTouchEvent(event); 905 onStopTrackingTouch(); 906 } 907 // ProgressBar doesn't know to repaint the thumb drawable 908 // in its inactive state when the touch stops (because the 909 // value has not apparently changed) 910 invalidate(); 911 break; 912 913 case MotionEvent.ACTION_CANCEL: 914 if (mIsDragging) { 915 onStopTrackingTouch(); 916 setPressed(false); 917 } 918 invalidate(); // see above explanation 919 break; 920 } 921 return true; 922 } 923 924 private void startDrag(MotionEvent event) { 925 setPressed(true); 926 927 if (mThumb != null) { 928 // This may be within the padding region. 929 invalidate(mThumb.getBounds()); 930 } 931 932 onStartTrackingTouch(); 933 trackTouchEvent(event); 934 attemptClaimDrag(); 935 } 936 937 private void setHotspot(float x, float y) { 938 final Drawable bg = getBackground(); 939 if (bg != null) { 940 bg.setHotspot(x, y); 941 } 942 } 943 944 @UnsupportedAppUsage 945 private void trackTouchEvent(MotionEvent event) { 946 final int x = Math.round(event.getX()); 947 final int y = Math.round(event.getY()); 948 final int width = getWidth(); 949 final int availableWidth = width - mPaddingLeft - mPaddingRight; 950 951 final float scale; 952 float progress = 0.0f; 953 if (isLayoutRtl() && mMirrorForRtl) { 954 if (x > width - mPaddingRight) { 955 scale = 0.0f; 956 } else if (x < mPaddingLeft) { 957 scale = 1.0f; 958 } else { 959 scale = (availableWidth - x + mPaddingLeft) / (float) availableWidth; 960 progress = mTouchProgressOffset; 961 } 962 } else { 963 if (x < mPaddingLeft) { 964 scale = 0.0f; 965 } else if (x > width - mPaddingRight) { 966 scale = 1.0f; 967 } else { 968 scale = (x - mPaddingLeft) / (float) availableWidth; 969 progress = mTouchProgressOffset; 970 } 971 } 972 973 final int range = getMax() - getMin(); 974 progress += scale * range + getMin(); 975 976 setHotspot(x, y); 977 setProgressInternal(Math.round(progress), true, false); 978 } 979 980 /** 981 * Tries to claim the user's drag motion, and requests disallowing any 982 * ancestors from stealing events in the drag. 983 */ 984 private void attemptClaimDrag() { 985 if (mParent != null) { 986 mParent.requestDisallowInterceptTouchEvent(true); 987 } 988 } 989 990 /** 991 * This is called when the user has started touching this widget. 992 */ 993 void onStartTrackingTouch() { 994 mIsDragging = true; 995 } 996 997 /** 998 * This is called when the user either releases his touch or the touch is 999 * canceled. 1000 */ 1001 void onStopTrackingTouch() { 1002 mIsDragging = false; 1003 } 1004 1005 /** 1006 * Called when the user changes the seekbar's progress by using a key event. 1007 */ 1008 void onKeyChange() { 1009 } 1010 1011 @Override 1012 public boolean onKeyDown(int keyCode, KeyEvent event) { 1013 if (isEnabled()) { 1014 int increment = mKeyProgressIncrement; 1015 switch (keyCode) { 1016 case KeyEvent.KEYCODE_DPAD_LEFT: 1017 case KeyEvent.KEYCODE_MINUS: 1018 increment = -increment; 1019 // fallthrough 1020 case KeyEvent.KEYCODE_DPAD_RIGHT: 1021 case KeyEvent.KEYCODE_PLUS: 1022 case KeyEvent.KEYCODE_EQUALS: 1023 increment = isLayoutRtl() ? -increment : increment; 1024 1025 if (setProgressInternal(getProgress() + increment, true, true)) { 1026 onKeyChange(); 1027 return true; 1028 } 1029 break; 1030 } 1031 } 1032 1033 return super.onKeyDown(keyCode, event); 1034 } 1035 1036 @Override 1037 public CharSequence getAccessibilityClassName() { 1038 return AbsSeekBar.class.getName(); 1039 } 1040 1041 /** @hide */ 1042 @Override 1043 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1044 super.onInitializeAccessibilityNodeInfoInternal(info); 1045 1046 if (isEnabled()) { 1047 final int progress = getProgress(); 1048 if (progress > getMin()) { 1049 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 1050 } 1051 if (progress < getMax()) { 1052 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 1053 } 1054 } 1055 } 1056 1057 /** @hide */ 1058 @Override 1059 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1060 if (super.performAccessibilityActionInternal(action, arguments)) { 1061 return true; 1062 } 1063 1064 if (!isEnabled()) { 1065 return false; 1066 } 1067 1068 switch (action) { 1069 case R.id.accessibilityActionSetProgress: { 1070 if (!canUserSetProgress()) { 1071 return false; 1072 } 1073 if (arguments == null || !arguments.containsKey( 1074 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) { 1075 return false; 1076 } 1077 float value = arguments.getFloat( 1078 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE); 1079 return setProgressInternal((int) value, true, true); 1080 } 1081 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 1082 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1083 if (!canUserSetProgress()) { 1084 return false; 1085 } 1086 int range = getMax() - getMin(); 1087 int increment = Math.max(1, Math.round((float) range / 20)); 1088 if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { 1089 increment = -increment; 1090 } 1091 1092 // Let progress bar handle clamping values. 1093 if (setProgressInternal(getProgress() + increment, true, true)) { 1094 onKeyChange(); 1095 return true; 1096 } 1097 return false; 1098 } 1099 } 1100 return false; 1101 } 1102 1103 /** 1104 * @return whether user can change progress on the view 1105 */ 1106 boolean canUserSetProgress() { 1107 return !isIndeterminate() && isEnabled(); 1108 } 1109 1110 @Override 1111 public void onRtlPropertiesChanged(int layoutDirection) { 1112 super.onRtlPropertiesChanged(layoutDirection); 1113 1114 final Drawable thumb = mThumb; 1115 if (thumb != null) { 1116 setThumbPos(getWidth(), thumb, getScale(), Integer.MIN_VALUE); 1117 1118 // Since we draw translated, the drawable's bounds that it signals 1119 // for invalidation won't be the actual bounds we want invalidated, 1120 // so just invalidate this whole view. 1121 invalidate(); 1122 } 1123 } 1124 } 1125