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 
AbsSeekBar(Context context)59     public AbsSeekBar(Context context) {
60         super(context);
61     }
62 
AbsSeekBar(Context context, AttributeSet attrs)63     public AbsSeekBar(Context context, AttributeSet attrs) {
64         super(context, attrs);
65     }
66 
AbsSeekBar(Context context, AttributeSet attrs, int defStyle)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      */
setThumb(Drawable thumb)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      */
getThumbOffset()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      */
setThumbOffset(int thumbOffset)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      */
setKeyProgressIncrement(int increment)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