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