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