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