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