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