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 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;
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    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