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