blob: 00ad580775fe2a3533fadf1248197480949b2a03 [file] [log] [blame]
Marco Nelissenf568b602011-06-28 08:52:55 -07001/*
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 com.android.musicfx.seekbar;
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;
Marco Nelissen0962a4b2011-12-08 14:53:23 -080027import android.view.ViewConfiguration;
Marco Nelissenf568b602011-06-28 08:52:55 -070028
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
Marco Nelissen2deae772011-06-28 09:04:34 -070044 boolean mIsVertical = false;
Marco Nelissenf568b602011-06-28 08:52:55 -070045 /**
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
Marco Nelissen0962a4b2011-12-08 14:53:23 -080054 private int mScaledTouchSlop;
55 private float mTouchDownX;
Marco Nelissend9fc0402012-04-05 09:57:03 -070056 private float mTouchDownY;
Marco Nelissen0962a4b2011-12-08 14:53:23 -080057 private boolean mIsDragging;
58
Marco Nelissenf568b602011-06-28 08:52:55 -070059 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();
Marco Nelissen0962a4b2011-12-08 14:53:23 -080084
85 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Marco Nelissenf568b602011-06-28 08:52:55 -070086 }
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.
Marco Nelissen2deae772011-06-28 09:04:34 -0700113 if (mIsVertical) {
114 mThumbOffset = thumb.getIntrinsicHeight() / 2;
115 } else {
116 mThumbOffset = thumb.getIntrinsicWidth() / 2;
117 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700118
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) {
Marco Nelissen2deae772011-06-28 09:04:34 -0700221 setThumbPos(getWidth(), getHeight(), thumb, scale, Integer.MIN_VALUE);
Marco Nelissenf568b602011-06-28 08:52:55 -0700222 /*
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;
Marco Nelissen2deae772011-06-28 09:04:34 -0700240 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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700270 }
271 } else {
Marco Nelissen2deae772011-06-28 09:04:34 -0700272 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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700301 }
302 }
303 }
304
305 /**
306 * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
307 */
Marco Nelissen2deae772011-06-28 09:04:34 -0700308 private void setThumbPos(int w, int h, Drawable thumb, float scale, int gap) {
309 int available;
Marco Nelissenf568b602011-06-28 08:52:55 -0700310 int thumbWidth = thumb.getIntrinsicWidth();
311 int thumbHeight = thumb.getIntrinsicHeight();
Marco Nelissen2deae772011-06-28 09:04:34 -0700312 if (mIsVertical) {
313 available = h - mPaddingTop - mPaddingBottom - thumbHeight;
314 } else {
315 available = w - mPaddingLeft - mPaddingRight - thumbWidth;
316 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700317
318 // The extra space for the thumb to move on the track
319 available += mThumbOffset * 2;
320
Marco Nelissenf568b602011-06-28 08:52:55 -0700321
Marco Nelissen2deae772011-06-28 09:04:34 -0700322 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);
Marco Nelissenf568b602011-06-28 08:52:55 -0700336 } else {
Marco Nelissen2deae772011-06-28 09:04:34 -0700337 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);
Marco Nelissenf568b602011-06-28 08:52:55 -0700350 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700351 }
352
353 @Override
354 protected synchronized void onDraw(Canvas canvas) {
355 super.onDraw(canvas);
356 if (mThumb != null) {
357 canvas.save();
Marco Nelissen2deae772011-06-28 09:04:34 -0700358 // Translate the padding. For the x/y, we need to allow the thumb to
Marco Nelissenf568b602011-06-28 08:52:55 -0700359 // draw in its extra space
Marco Nelissen2deae772011-06-28 09:04:34 -0700360 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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700369 }
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));
Marco Nelissen2deae772011-06-28 09:04:34 -0700389
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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700395 }
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:
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800405 if (isInScrollingContainer()) {
406 mTouchDownX = event.getX();
Marco Nelissend9fc0402012-04-05 09:57:03 -0700407 mTouchDownY = event.getY();
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800408 } 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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700417 break;
418
419 case MotionEvent.ACTION_MOVE:
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800420 if (mIsDragging) {
421 trackTouchEvent(event);
422 } else {
423 final float x = event.getX();
Marco Nelissend9fc0402012-04-05 09:57:03 -0700424 final float y = event.getX();
425 if (Math.abs(mIsVertical ?
426 (y - mTouchDownY) : (x - mTouchDownX)) > mScaledTouchSlop) {
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800427 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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700436 break;
437
438 case MotionEvent.ACTION_UP:
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800439 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 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700450 // 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:
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800457 if (mIsDragging) {
458 onStopTrackingTouch();
459 setPressed(false);
460 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700461 invalidate(); // see above explanation
462 break;
463 }
464 return true;
465 }
466
467 private void trackTouchEvent(MotionEvent event) {
Marco Nelissenf568b602011-06-28 08:52:55 -0700468 float progress = 0;
Marco Nelissen2deae772011-06-28 09:04:34 -0700469 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;
Marco Nelissenf568b602011-06-28 08:52:55 -0700485 } else {
Marco Nelissen2deae772011-06-28 09:04:34 -0700486 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;
Marco Nelissenf568b602011-06-28 08:52:55 -0700501 }
502
Marco Nelissenf568b602011-06-28 08:52:55 -0700503 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() {
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800520 mIsDragging = true;
Marco Nelissenf568b602011-06-28 08:52:55 -0700521 }
522
523 /**
524 * This is called when the user either releases his touch or the touch is
525 * canceled.
526 */
527 void onStopTrackingTouch() {
Marco Nelissen0962a4b2011-12-08 14:53:23 -0800528 mIsDragging = false;
Marco Nelissenf568b602011-06-28 08:52:55 -0700529 }
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();
Marco Nelissen2deae772011-06-28 09:04:34 -0700541 if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !mIsVertical)
542 || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && mIsVertical)) {
543 if (progress > 0) {
Marco Nelissenf568b602011-06-28 08:52:55 -0700544 setProgress(progress - mKeyProgressIncrement, true);
545 onKeyChange();
546 return true;
Marco Nelissen2deae772011-06-28 09:04:34 -0700547 }
548 } else if ((keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && !mIsVertical)
549 || (keyCode == KeyEvent.KEYCODE_DPAD_UP && mIsVertical)) {
550 if (progress < getMax()) {
Marco Nelissenf568b602011-06-28 08:52:55 -0700551 setProgress(progress + mKeyProgressIncrement, true);
552 onKeyChange();
553 return true;
Marco Nelissen2deae772011-06-28 09:04:34 -0700554 }
Marco Nelissenf568b602011-06-28 08:52:55 -0700555 }
556 }
557
558 return super.onKeyDown(keyCode, event);
559 }
560
561}