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