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