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