AbsSeekBar.java revision 06849e8f5368831086b0c33f9037a015fb00e864
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 17package android.widget; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Canvas; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.os.Bundle; 25import android.util.AttributeSet; 26import android.view.KeyEvent; 27import android.view.MotionEvent; 28import android.view.ViewConfiguration; 29import android.view.accessibility.AccessibilityEvent; 30import android.view.accessibility.AccessibilityNodeInfo; 31 32public 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(getResolvedLayoutDirection()); 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 updateThumbPos(w, h); 245 } 246 247 private void updateThumbPos(int w, int h) { 248 Drawable d = getCurrentDrawable(); 249 Drawable thumb = mThumb; 250 int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); 251 // The max height does not incorporate padding, whereas the height 252 // parameter does 253 int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); 254 255 int max = getMax(); 256 float scale = max > 0 ? (float) getProgress() / (float) max : 0; 257 258 if (thumbHeight > trackHeight) { 259 if (thumb != null) { 260 setThumbPos(w, thumb, scale, 0); 261 } 262 int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; 263 if (d != null) { 264 // Canvas will be translated by the padding, so 0,0 is where we start drawing 265 d.setBounds(0, gapForCenteringTrack, 266 w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack 267 - mPaddingTop); 268 } 269 } else { 270 if (d != null) { 271 // Canvas will be translated by the padding, so 0,0 is where we start drawing 272 d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom 273 - mPaddingTop); 274 } 275 int gap = (trackHeight - thumbHeight) / 2; 276 if (thumb != null) { 277 setThumbPos(w, thumb, scale, gap); 278 } 279 } 280 } 281 282 /** 283 * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and 284 */ 285 private void setThumbPos(int w, Drawable thumb, float scale, int gap) { 286 int available = w - mPaddingLeft - mPaddingRight; 287 int thumbWidth = thumb.getIntrinsicWidth(); 288 int thumbHeight = thumb.getIntrinsicHeight(); 289 available -= thumbWidth; 290 291 // The extra space for the thumb to move on the track 292 available += mThumbOffset * 2; 293 294 int thumbPos = (int) (scale * available); 295 296 int topBound, bottomBound; 297 if (gap == Integer.MIN_VALUE) { 298 Rect oldBounds = thumb.getBounds(); 299 topBound = oldBounds.top; 300 bottomBound = oldBounds.bottom; 301 } else { 302 topBound = gap; 303 bottomBound = gap + thumbHeight; 304 } 305 306 // Canvas will be translated, so 0,0 is where we start drawing 307 final int left = isLayoutRtl() ? available - thumbPos : thumbPos; 308 thumb.setBounds(left, topBound, left + thumbWidth, bottomBound); 309 } 310 311 @Override 312 public void onResolveDrawables(int layoutDirection) { 313 super.onResolveDrawables(layoutDirection); 314 315 if (mThumb != null) { 316 mThumb.setLayoutDirection(layoutDirection); 317 } 318 } 319 320 @Override 321 protected synchronized void onDraw(Canvas canvas) { 322 super.onDraw(canvas); 323 if (mThumb != null) { 324 canvas.save(); 325 // Translate the padding. For the x, we need to allow the thumb to 326 // draw in its extra space 327 canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop); 328 mThumb.draw(canvas); 329 canvas.restore(); 330 } 331 } 332 333 @Override 334 protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 335 Drawable d = getCurrentDrawable(); 336 337 int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight(); 338 int dw = 0; 339 int dh = 0; 340 if (d != null) { 341 dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); 342 dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); 343 dh = Math.max(thumbHeight, dh); 344 } 345 dw += mPaddingLeft + mPaddingRight; 346 dh += mPaddingTop + mPaddingBottom; 347 348 setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), 349 resolveSizeAndState(dh, heightMeasureSpec, 0)); 350 } 351 352 @Override 353 public boolean onTouchEvent(MotionEvent event) { 354 if (!mIsUserSeekable || !isEnabled()) { 355 return false; 356 } 357 358 switch (event.getAction()) { 359 case MotionEvent.ACTION_DOWN: 360 if (isInScrollingContainer()) { 361 mTouchDownX = event.getX(); 362 } else { 363 setPressed(true); 364 if (mThumb != null) { 365 invalidate(mThumb.getBounds()); // This may be within the padding region 366 } 367 onStartTrackingTouch(); 368 trackTouchEvent(event); 369 attemptClaimDrag(); 370 } 371 break; 372 373 case MotionEvent.ACTION_MOVE: 374 if (mIsDragging) { 375 trackTouchEvent(event); 376 } else { 377 final float x = event.getX(); 378 if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { 379 setPressed(true); 380 if (mThumb != null) { 381 invalidate(mThumb.getBounds()); // This may be within the padding region 382 } 383 onStartTrackingTouch(); 384 trackTouchEvent(event); 385 attemptClaimDrag(); 386 } 387 } 388 break; 389 390 case MotionEvent.ACTION_UP: 391 if (mIsDragging) { 392 trackTouchEvent(event); 393 onStopTrackingTouch(); 394 setPressed(false); 395 } else { 396 // Touch up when we never crossed the touch slop threshold should 397 // be interpreted as a tap-seek to that location. 398 onStartTrackingTouch(); 399 trackTouchEvent(event); 400 onStopTrackingTouch(); 401 } 402 // ProgressBar doesn't know to repaint the thumb drawable 403 // in its inactive state when the touch stops (because the 404 // value has not apparently changed) 405 invalidate(); 406 break; 407 408 case MotionEvent.ACTION_CANCEL: 409 if (mIsDragging) { 410 onStopTrackingTouch(); 411 setPressed(false); 412 } 413 invalidate(); // see above explanation 414 break; 415 } 416 return true; 417 } 418 419 private void trackTouchEvent(MotionEvent event) { 420 final int width = getWidth(); 421 final int available = width - mPaddingLeft - mPaddingRight; 422 int x = (int)event.getX(); 423 float scale; 424 float progress = 0; 425 if (isLayoutRtl()) { 426 if (x > width - mPaddingRight) { 427 scale = 0.0f; 428 } else if (x < mPaddingLeft) { 429 scale = 1.0f; 430 } else { 431 scale = (float)(available - x + mPaddingLeft) / (float)available; 432 progress = mTouchProgressOffset; 433 } 434 } else { 435 if (x < mPaddingLeft) { 436 scale = 0.0f; 437 } else if (x > width - mPaddingRight) { 438 scale = 1.0f; 439 } else { 440 scale = (float)(x - mPaddingLeft) / (float)available; 441 progress = mTouchProgressOffset; 442 } 443 } 444 final int max = getMax(); 445 progress += scale * max; 446 447 setProgress((int) progress, true); 448 } 449 450 /** 451 * Tries to claim the user's drag motion, and requests disallowing any 452 * ancestors from stealing events in the drag. 453 */ 454 private void attemptClaimDrag() { 455 if (mParent != null) { 456 mParent.requestDisallowInterceptTouchEvent(true); 457 } 458 } 459 460 /** 461 * This is called when the user has started touching this widget. 462 */ 463 void onStartTrackingTouch() { 464 mIsDragging = true; 465 } 466 467 /** 468 * This is called when the user either releases his touch or the touch is 469 * canceled. 470 */ 471 void onStopTrackingTouch() { 472 mIsDragging = false; 473 } 474 475 /** 476 * Called when the user changes the seekbar's progress by using a key event. 477 */ 478 void onKeyChange() { 479 } 480 481 @Override 482 public boolean onKeyDown(int keyCode, KeyEvent event) { 483 if (isEnabled()) { 484 int progress = getProgress(); 485 switch (keyCode) { 486 case KeyEvent.KEYCODE_DPAD_LEFT: 487 if (progress <= 0) break; 488 setProgress(progress - mKeyProgressIncrement, true); 489 onKeyChange(); 490 return true; 491 492 case KeyEvent.KEYCODE_DPAD_RIGHT: 493 if (progress >= getMax()) break; 494 setProgress(progress + mKeyProgressIncrement, true); 495 onKeyChange(); 496 return true; 497 } 498 } 499 500 return super.onKeyDown(keyCode, event); 501 } 502 503 @Override 504 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 505 super.onInitializeAccessibilityEvent(event); 506 event.setClassName(AbsSeekBar.class.getName()); 507 } 508 509 @Override 510 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 511 super.onInitializeAccessibilityNodeInfo(info); 512 info.setClassName(AbsSeekBar.class.getName()); 513 514 if (isEnabled()) { 515 final int progress = getProgress(); 516 if (progress > 0) { 517 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 518 } 519 if (progress < getMax()) { 520 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 521 } 522 } 523 } 524 525 @Override 526 public boolean performAccessibilityAction(int action, Bundle arguments) { 527 if (super.performAccessibilityAction(action, arguments)) { 528 return true; 529 } 530 if (!isEnabled()) { 531 return false; 532 } 533 final int progress = getProgress(); 534 final int increment = Math.max(1, Math.round((float) getMax() / 5)); 535 switch (action) { 536 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 537 if (progress <= 0) { 538 return false; 539 } 540 setProgress(progress - increment, true); 541 onKeyChange(); 542 return true; 543 } 544 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 545 if (progress >= getMax()) { 546 return false; 547 } 548 setProgress(progress + increment, true); 549 onKeyChange(); 550 return true; 551 } 552 } 553 return false; 554 } 555} 556