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.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.Canvas; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.os.Bundle; 25 import android.util.AttributeSet; 26 import android.view.KeyEvent; 27 import android.view.MotionEvent; 28 import android.view.ViewConfiguration; 29 import android.view.accessibility.AccessibilityEvent; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 32 public abstract class AbsSeekBar extends ProgressBar { 33 private Drawable mThumb; 34 private int mThumbOffset; 35 36 /** 37 * On touch, this offset plus the scaled value from the position of the 38 * touch will form the progress value. Usually 0. 39 */ 40 float mTouchProgressOffset; 41 42 /** 43 * Whether this is user seekable. 44 */ 45 boolean mIsUserSeekable = true; 46 47 /** 48 * On key presses (right or left), the amount to increment/decrement the 49 * progress. 50 */ 51 private int mKeyProgressIncrement = 1; 52 53 private static final int NO_ALPHA = 0xFF; 54 private float mDisabledAlpha; 55 56 private int mScaledTouchSlop; 57 private float mTouchDownX; 58 private boolean mIsDragging; 59 60 public AbsSeekBar(Context context) { 61 super(context); 62 } 63 64 public AbsSeekBar(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 } 67 68 public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) { 69 super(context, attrs, defStyle); 70 71 TypedArray a = context.obtainStyledAttributes(attrs, 72 com.android.internal.R.styleable.SeekBar, defStyle, 0); 73 Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); 74 setThumb(thumb); // will guess mThumbOffset if thumb != null... 75 // ...but allow layout to override this 76 int thumbOffset = a.getDimensionPixelOffset( 77 com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset()); 78 setThumbOffset(thumbOffset); 79 a.recycle(); 80 81 a = context.obtainStyledAttributes(attrs, 82 com.android.internal.R.styleable.Theme, 0, 0); 83 mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f); 84 a.recycle(); 85 86 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 87 } 88 89 /** 90 * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar. 91 * <p> 92 * If the thumb is a valid drawable (i.e. not null), half its width will be 93 * used as the new thumb offset (@see #setThumbOffset(int)). 94 * 95 * @param thumb Drawable representing the thumb 96 */ 97 public void setThumb(Drawable thumb) { 98 boolean needUpdate; 99 // This way, calling setThumb again with the same bitmap will result in 100 // it recalcuating mThumbOffset (if for example it the bounds of the 101 // drawable changed) 102 if (mThumb != null && thumb != mThumb) { 103 mThumb.setCallback(null); 104 needUpdate = true; 105 } else { 106 needUpdate = false; 107 } 108 if (thumb != null) { 109 thumb.setCallback(this); 110 if (canResolveLayoutDirection()) { 111 thumb.setLayoutDirection(getLayoutDirection()); 112 } 113 114 // Assuming the thumb drawable is symmetric, set the thumb offset 115 // such that the thumb will hang halfway off either edge of the 116 // progress bar. 117 mThumbOffset = thumb.getIntrinsicWidth() / 2; 118 119 // If we're updating get the new states 120 if (needUpdate && 121 (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth() 122 || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) { 123 requestLayout(); 124 } 125 } 126 mThumb = thumb; 127 invalidate(); 128 if (needUpdate) { 129 updateThumbPos(getWidth(), getHeight()); 130 if (thumb != null && thumb.isStateful()) { 131 // Note that if the states are different this won't work. 132 // For now, let's consider that an app bug. 133 int[] state = getDrawableState(); 134 thumb.setState(state); 135 } 136 } 137 } 138 139 /** 140 * Return the drawable used to represent the scroll thumb - the component that 141 * the user can drag back and forth indicating the current value by its position. 142 * 143 * @return The current thumb drawable 144 */ 145 public Drawable getThumb() { 146 return mThumb; 147 } 148 149 /** 150 * @see #setThumbOffset(int) 151 */ 152 public int getThumbOffset() { 153 return mThumbOffset; 154 } 155 156 /** 157 * Sets the thumb offset that allows the thumb to extend out of the range of 158 * the track. 159 * 160 * @param thumbOffset The offset amount in pixels. 161 */ 162 public void setThumbOffset(int thumbOffset) { 163 mThumbOffset = thumbOffset; 164 invalidate(); 165 } 166 167 /** 168 * Sets the amount of progress changed via the arrow keys. 169 * 170 * @param increment The amount to increment or decrement when the user 171 * presses the arrow keys. 172 */ 173 public void setKeyProgressIncrement(int increment) { 174 mKeyProgressIncrement = increment < 0 ? -increment : increment; 175 } 176 177 /** 178 * Returns the amount of progress changed via the arrow keys. 179 * <p> 180 * By default, this will be a value that is derived from the max progress. 181 * 182 * @return The amount to increment or decrement when the user presses the 183 * arrow keys. This will be positive. 184 */ 185 public int getKeyProgressIncrement() { 186 return mKeyProgressIncrement; 187 } 188 189 @Override 190 public synchronized void setMax(int max) { 191 super.setMax(max); 192 193 if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) { 194 // It will take the user too long to change this via keys, change it 195 // to something more reasonable 196 setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20))); 197 } 198 } 199 200 @Override 201 protected boolean verifyDrawable(Drawable who) { 202 return who == mThumb || super.verifyDrawable(who); 203 } 204 205 @Override 206 public void jumpDrawablesToCurrentState() { 207 super.jumpDrawablesToCurrentState(); 208 if (mThumb != null) mThumb.jumpToCurrentState(); 209 } 210 211 @Override 212 protected void drawableStateChanged() { 213 super.drawableStateChanged(); 214 215 Drawable progressDrawable = getProgressDrawable(); 216 if (progressDrawable != null) { 217 progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); 218 } 219 220 if (mThumb != null && mThumb.isStateful()) { 221 int[] state = getDrawableState(); 222 mThumb.setState(state); 223 } 224 } 225 226 @Override 227 void onProgressRefresh(float scale, boolean fromUser) { 228 super.onProgressRefresh(scale, fromUser); 229 Drawable thumb = mThumb; 230 if (thumb != null) { 231 setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); 232 /* 233 * Since we draw translated, the drawable's bounds that it signals 234 * for invalidation won't be the actual bounds we want invalidated, 235 * so just invalidate this whole view. 236 */ 237 invalidate(); 238 } 239 } 240 241 242 @Override 243 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 244 super.onSizeChanged(w, h, oldw, oldh); 245 updateThumbPos(w, h); 246 } 247 248 private void updateThumbPos(int w, int h) { 249 Drawable d = getCurrentDrawable(); 250 Drawable thumb = mThumb; 251 int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); 252 // The max height does not incorporate padding, whereas the height 253 // parameter does 254 int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); 255 256 int max = getMax(); 257 float scale = max > 0 ? (float) getProgress() / (float) max : 0; 258 259 if (thumbHeight > trackHeight) { 260 if (thumb != null) { 261 setThumbPos(w, thumb, scale, 0); 262 } 263 int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; 264 if (d != null) { 265 // Canvas will be translated by the padding, so 0,0 is where we start drawing 266 d.setBounds(0, gapForCenteringTrack, 267 w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack 268 - mPaddingTop); 269 } 270 } else { 271 if (d != null) { 272 // Canvas will be translated by the padding, so 0,0 is where we start drawing 273 d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom 274 - mPaddingTop); 275 } 276 int gap = (trackHeight - thumbHeight) / 2; 277 if (thumb != null) { 278 setThumbPos(w, thumb, scale, gap); 279 } 280 } 281 } 282 283 /** 284 * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and 285 */ 286 private void setThumbPos(int w, Drawable thumb, float scale, int gap) { 287 int available = w - mPaddingLeft - mPaddingRight; 288 int thumbWidth = thumb.getIntrinsicWidth(); 289 int thumbHeight = thumb.getIntrinsicHeight(); 290 available -= thumbWidth; 291 292 // The extra space for the thumb to move on the track 293 available += mThumbOffset * 2; 294 295 int thumbPos = (int) (scale * available); 296 297 int topBound, bottomBound; 298 if (gap == Integer.MIN_VALUE) { 299 Rect oldBounds = thumb.getBounds(); 300 topBound = oldBounds.top; 301 bottomBound = oldBounds.bottom; 302 } else { 303 topBound = gap; 304 bottomBound = gap + thumbHeight; 305 } 306 307 // Canvas will be translated, so 0,0 is where we start drawing 308 final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos; 309 thumb.setBounds(left, topBound, left + thumbWidth, bottomBound); 310 } 311 312 /** 313 * @hide 314 */ 315 @Override 316 public void onResolveDrawables(int layoutDirection) { 317 super.onResolveDrawables(layoutDirection); 318 319 if (mThumb != null) { 320 mThumb.setLayoutDirection(layoutDirection); 321 } 322 } 323 324 @Override 325 protected synchronized void onDraw(Canvas canvas) { 326 super.onDraw(canvas); 327 if (mThumb != null) { 328 canvas.save(); 329 // Translate the padding. For the x, we need to allow the thumb to 330 // draw in its extra space 331 canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop); 332 mThumb.draw(canvas); 333 canvas.restore(); 334 } 335 } 336 337 @Override 338 protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 339 Drawable d = getCurrentDrawable(); 340 341 int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight(); 342 int dw = 0; 343 int dh = 0; 344 if (d != null) { 345 dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); 346 dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); 347 dh = Math.max(thumbHeight, dh); 348 } 349 dw += mPaddingLeft + mPaddingRight; 350 dh += mPaddingTop + mPaddingBottom; 351 352 setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), 353 resolveSizeAndState(dh, heightMeasureSpec, 0)); 354 } 355 356 @Override 357 public boolean onTouchEvent(MotionEvent event) { 358 if (!mIsUserSeekable || !isEnabled()) { 359 return false; 360 } 361 362 switch (event.getAction()) { 363 case MotionEvent.ACTION_DOWN: 364 if (isInScrollingContainer()) { 365 mTouchDownX = event.getX(); 366 } else { 367 setPressed(true); 368 if (mThumb != null) { 369 invalidate(mThumb.getBounds()); // This may be within the padding region 370 } 371 onStartTrackingTouch(); 372 trackTouchEvent(event); 373 attemptClaimDrag(); 374 } 375 break; 376 377 case MotionEvent.ACTION_MOVE: 378 if (mIsDragging) { 379 trackTouchEvent(event); 380 } else { 381 final float x = event.getX(); 382 if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { 383 setPressed(true); 384 if (mThumb != null) { 385 invalidate(mThumb.getBounds()); // This may be within the padding region 386 } 387 onStartTrackingTouch(); 388 trackTouchEvent(event); 389 attemptClaimDrag(); 390 } 391 } 392 break; 393 394 case MotionEvent.ACTION_UP: 395 if (mIsDragging) { 396 trackTouchEvent(event); 397 onStopTrackingTouch(); 398 setPressed(false); 399 } else { 400 // Touch up when we never crossed the touch slop threshold should 401 // be interpreted as a tap-seek to that location. 402 onStartTrackingTouch(); 403 trackTouchEvent(event); 404 onStopTrackingTouch(); 405 } 406 // ProgressBar doesn't know to repaint the thumb drawable 407 // in its inactive state when the touch stops (because the 408 // value has not apparently changed) 409 invalidate(); 410 break; 411 412 case MotionEvent.ACTION_CANCEL: 413 if (mIsDragging) { 414 onStopTrackingTouch(); 415 setPressed(false); 416 } 417 invalidate(); // see above explanation 418 break; 419 } 420 return true; 421 } 422 423 private void trackTouchEvent(MotionEvent event) { 424 final int width = getWidth(); 425 final int available = width - mPaddingLeft - mPaddingRight; 426 int x = (int)event.getX(); 427 float scale; 428 float progress = 0; 429 if (isLayoutRtl() && mMirrorForRtl) { 430 if (x > width - mPaddingRight) { 431 scale = 0.0f; 432 } else if (x < mPaddingLeft) { 433 scale = 1.0f; 434 } else { 435 scale = (float)(available - x + mPaddingLeft) / (float)available; 436 progress = mTouchProgressOffset; 437 } 438 } else { 439 if (x < mPaddingLeft) { 440 scale = 0.0f; 441 } else if (x > width - mPaddingRight) { 442 scale = 1.0f; 443 } else { 444 scale = (float)(x - mPaddingLeft) / (float)available; 445 progress = mTouchProgressOffset; 446 } 447 } 448 final int max = getMax(); 449 progress += scale * max; 450 451 setProgress((int) progress, true); 452 } 453 454 /** 455 * Tries to claim the user's drag motion, and requests disallowing any 456 * ancestors from stealing events in the drag. 457 */ 458 private void attemptClaimDrag() { 459 if (mParent != null) { 460 mParent.requestDisallowInterceptTouchEvent(true); 461 } 462 } 463 464 /** 465 * This is called when the user has started touching this widget. 466 */ 467 void onStartTrackingTouch() { 468 mIsDragging = true; 469 } 470 471 /** 472 * This is called when the user either releases his touch or the touch is 473 * canceled. 474 */ 475 void onStopTrackingTouch() { 476 mIsDragging = false; 477 } 478 479 /** 480 * Called when the user changes the seekbar's progress by using a key event. 481 */ 482 void onKeyChange() { 483 } 484 485 @Override 486 public boolean onKeyDown(int keyCode, KeyEvent event) { 487 if (isEnabled()) { 488 int progress = getProgress(); 489 switch (keyCode) { 490 case KeyEvent.KEYCODE_DPAD_LEFT: 491 if (progress <= 0) break; 492 setProgress(progress - mKeyProgressIncrement, true); 493 onKeyChange(); 494 return true; 495 496 case KeyEvent.KEYCODE_DPAD_RIGHT: 497 if (progress >= getMax()) break; 498 setProgress(progress + mKeyProgressIncrement, true); 499 onKeyChange(); 500 return true; 501 } 502 } 503 504 return super.onKeyDown(keyCode, event); 505 } 506 507 @Override 508 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 509 super.onInitializeAccessibilityEvent(event); 510 event.setClassName(AbsSeekBar.class.getName()); 511 } 512 513 @Override 514 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 515 super.onInitializeAccessibilityNodeInfo(info); 516 info.setClassName(AbsSeekBar.class.getName()); 517 518 if (isEnabled()) { 519 final int progress = getProgress(); 520 if (progress > 0) { 521 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 522 } 523 if (progress < getMax()) { 524 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 525 } 526 } 527 } 528 529 @Override 530 public boolean performAccessibilityAction(int action, Bundle arguments) { 531 if (super.performAccessibilityAction(action, arguments)) { 532 return true; 533 } 534 if (!isEnabled()) { 535 return false; 536 } 537 final int progress = getProgress(); 538 final int increment = Math.max(1, Math.round((float) getMax() / 5)); 539 switch (action) { 540 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 541 if (progress <= 0) { 542 return false; 543 } 544 setProgress(progress - increment, true); 545 onKeyChange(); 546 return true; 547 } 548 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 549 if (progress >= getMax()) { 550 return false; 551 } 552 setProgress(progress + increment, true); 553 onKeyChange(); 554 return true; 555 } 556 } 557 return false; 558 } 559 560 @Override 561 public void onRtlPropertiesChanged(int layoutDirection) { 562 super.onRtlPropertiesChanged(layoutDirection); 563 564 int max = getMax(); 565 float scale = max > 0 ? (float) getProgress() / (float) max : 0; 566 567 Drawable thumb = mThumb; 568 if (thumb != null) { 569 setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); 570 /* 571 * Since we draw translated, the drawable's bounds that it signals 572 * for invalidation won't be the actual bounds we want invalidated, 573 * so just invalidate this whole view. 574 */ 575 invalidate(); 576 } 577 } 578 } 579