AbsSeekBar.java revision 06849e8f5368831086b0c33f9037a015fb00e864
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.os.Bundle;
25import android.util.AttributeSet;
26import android.view.KeyEvent;
27import android.view.MotionEvent;
28import android.view.ViewConfiguration;
29import android.view.accessibility.AccessibilityEvent;
30import android.view.accessibility.AccessibilityNodeInfo;
31
32public abstract class AbsSeekBar extends ProgressBar {
33    private Drawable mThumb;
34    private int mThumbOffset;
35
36    /**
37     * On touch, this offset plus the scaled value from the position of the
38     * touch will form the progress value. Usually 0.
39     */
40    float mTouchProgressOffset;
41
42    /**
43     * Whether this is user seekable.
44     */
45    boolean mIsUserSeekable = true;
46
47    /**
48     * On key presses (right or left), the amount to increment/decrement the
49     * progress.
50     */
51    private int mKeyProgressIncrement = 1;
52
53    private static final int NO_ALPHA = 0xFF;
54    private float mDisabledAlpha;
55
56    private int mScaledTouchSlop;
57    private float mTouchDownX;
58    private boolean mIsDragging;
59
60    public AbsSeekBar(Context context) {
61        super(context);
62    }
63
64    public AbsSeekBar(Context context, AttributeSet attrs) {
65        super(context, attrs);
66    }
67
68    public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) {
69        super(context, attrs, defStyle);
70
71        TypedArray a = context.obtainStyledAttributes(attrs,
72                com.android.internal.R.styleable.SeekBar, defStyle, 0);
73        Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb);
74        setThumb(thumb); // will guess mThumbOffset if thumb != null...
75        // ...but allow layout to override this
76        int thumbOffset = a.getDimensionPixelOffset(
77                com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset());
78        setThumbOffset(thumbOffset);
79        a.recycle();
80
81        a = context.obtainStyledAttributes(attrs,
82                com.android.internal.R.styleable.Theme, 0, 0);
83        mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);
84        a.recycle();
85
86        mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
87    }
88
89    /**
90     * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
91     * <p>
92     * If the thumb is a valid drawable (i.e. not null), half its width will be
93     * used as the new thumb offset (@see #setThumbOffset(int)).
94     *
95     * @param thumb Drawable representing the thumb
96     */
97    public void setThumb(Drawable thumb) {
98        boolean needUpdate;
99        // This way, calling setThumb again with the same bitmap will result in
100        // it recalcuating mThumbOffset (if for example it the bounds of the
101        // drawable changed)
102        if (mThumb != null && thumb != mThumb) {
103            mThumb.setCallback(null);
104            needUpdate = true;
105        } else {
106            needUpdate = false;
107        }
108        if (thumb != null) {
109            thumb.setCallback(this);
110            if (canResolveLayoutDirection()) {
111                thumb.setLayoutDirection(getResolvedLayoutDirection());
112            }
113
114            // Assuming the thumb drawable is symmetric, set the thumb offset
115            // such that the thumb will hang halfway off either edge of the
116            // progress bar.
117            mThumbOffset = thumb.getIntrinsicWidth() / 2;
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 != null && 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     * Return the drawable used to represent the scroll thumb - the component that
141     * the user can drag back and forth indicating the current value by its position.
142     *
143     * @return The current thumb drawable
144     */
145    public Drawable getThumb() {
146        return mThumb;
147    }
148
149    /**
150     * @see #setThumbOffset(int)
151     */
152    public int getThumbOffset() {
153        return mThumbOffset;
154    }
155
156    /**
157     * Sets the thumb offset that allows the thumb to extend out of the range of
158     * the track.
159     *
160     * @param thumbOffset The offset amount in pixels.
161     */
162    public void setThumbOffset(int thumbOffset) {
163        mThumbOffset = thumbOffset;
164        invalidate();
165    }
166
167    /**
168     * Sets the amount of progress changed via the arrow keys.
169     *
170     * @param increment The amount to increment or decrement when the user
171     *            presses the arrow keys.
172     */
173    public void setKeyProgressIncrement(int increment) {
174        mKeyProgressIncrement = increment < 0 ? -increment : increment;
175    }
176
177    /**
178     * Returns the amount of progress changed via the arrow keys.
179     * <p>
180     * By default, this will be a value that is derived from the max progress.
181     *
182     * @return The amount to increment or decrement when the user presses the
183     *         arrow keys. This will be positive.
184     */
185    public int getKeyProgressIncrement() {
186        return mKeyProgressIncrement;
187    }
188
189    @Override
190    public synchronized void setMax(int max) {
191        super.setMax(max);
192
193        if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) {
194            // It will take the user too long to change this via keys, change it
195            // to something more reasonable
196            setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20)));
197        }
198    }
199
200    @Override
201    protected boolean verifyDrawable(Drawable who) {
202        return who == mThumb || super.verifyDrawable(who);
203    }
204
205    @Override
206    public void jumpDrawablesToCurrentState() {
207        super.jumpDrawablesToCurrentState();
208        if (mThumb != null) mThumb.jumpToCurrentState();
209    }
210
211    @Override
212    protected void drawableStateChanged() {
213        super.drawableStateChanged();
214
215        Drawable progressDrawable = getProgressDrawable();
216        if (progressDrawable != null) {
217            progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
218        }
219
220        if (mThumb != null && mThumb.isStateful()) {
221            int[] state = getDrawableState();
222            mThumb.setState(state);
223        }
224    }
225
226    @Override
227    void onProgressRefresh(float scale, boolean fromUser) {
228        super.onProgressRefresh(scale, fromUser);
229        Drawable thumb = mThumb;
230        if (thumb != null) {
231            setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE);
232            /*
233             * Since we draw translated, the drawable's bounds that it signals
234             * for invalidation won't be the actual bounds we want invalidated,
235             * so just invalidate this whole view.
236             */
237            invalidate();
238        }
239    }
240
241
242    @Override
243    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
244        updateThumbPos(w, h);
245    }
246
247    private void updateThumbPos(int w, int h) {
248        Drawable d = getCurrentDrawable();
249        Drawable thumb = mThumb;
250        int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
251        // The max height does not incorporate padding, whereas the height
252        // parameter does
253        int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
254
255        int max = getMax();
256        float scale = max > 0 ? (float) getProgress() / (float) max : 0;
257
258        if (thumbHeight > trackHeight) {
259            if (thumb != null) {
260                setThumbPos(w, thumb, scale, 0);
261            }
262            int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
263            if (d != null) {
264                // Canvas will be translated by the padding, so 0,0 is where we start drawing
265                d.setBounds(0, gapForCenteringTrack,
266                        w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
267                        - mPaddingTop);
268            }
269        } else {
270            if (d != null) {
271                // Canvas will be translated by the padding, so 0,0 is where we start drawing
272                d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
273                        - mPaddingTop);
274            }
275            int gap = (trackHeight - thumbHeight) / 2;
276            if (thumb != null) {
277                setThumbPos(w, thumb, scale, gap);
278            }
279        }
280    }
281
282    /**
283     * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
284     */
285    private void setThumbPos(int w, Drawable thumb, float scale, int gap) {
286        int available = w - mPaddingLeft - mPaddingRight;
287        int thumbWidth = thumb.getIntrinsicWidth();
288        int thumbHeight = thumb.getIntrinsicHeight();
289        available -= thumbWidth;
290
291        // The extra space for the thumb to move on the track
292        available += mThumbOffset * 2;
293
294        int thumbPos = (int) (scale * available);
295
296        int topBound, bottomBound;
297        if (gap == Integer.MIN_VALUE) {
298            Rect oldBounds = thumb.getBounds();
299            topBound = oldBounds.top;
300            bottomBound = oldBounds.bottom;
301        } else {
302            topBound = gap;
303            bottomBound = gap + thumbHeight;
304        }
305
306        // Canvas will be translated, so 0,0 is where we start drawing
307        final int left = isLayoutRtl() ? available - thumbPos : thumbPos;
308        thumb.setBounds(left, topBound, left + thumbWidth, bottomBound);
309    }
310
311    @Override
312    public void onResolveDrawables(int layoutDirection) {
313        super.onResolveDrawables(layoutDirection);
314
315        if (mThumb != null) {
316            mThumb.setLayoutDirection(layoutDirection);
317        }
318    }
319
320    @Override
321    protected synchronized void onDraw(Canvas canvas) {
322        super.onDraw(canvas);
323        if (mThumb != null) {
324            canvas.save();
325            // Translate the padding. For the x, we need to allow the thumb to
326            // draw in its extra space
327            canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
328            mThumb.draw(canvas);
329            canvas.restore();
330        }
331    }
332
333    @Override
334    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
335        Drawable d = getCurrentDrawable();
336
337        int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
338        int dw = 0;
339        int dh = 0;
340        if (d != null) {
341            dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
342            dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
343            dh = Math.max(thumbHeight, dh);
344        }
345        dw += mPaddingLeft + mPaddingRight;
346        dh += mPaddingTop + mPaddingBottom;
347
348        setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
349                resolveSizeAndState(dh, heightMeasureSpec, 0));
350    }
351
352    @Override
353    public boolean onTouchEvent(MotionEvent event) {
354        if (!mIsUserSeekable || !isEnabled()) {
355            return false;
356        }
357
358        switch (event.getAction()) {
359            case MotionEvent.ACTION_DOWN:
360                if (isInScrollingContainer()) {
361                    mTouchDownX = event.getX();
362                } else {
363                    setPressed(true);
364                    if (mThumb != null) {
365                        invalidate(mThumb.getBounds()); // This may be within the padding region
366                    }
367                    onStartTrackingTouch();
368                    trackTouchEvent(event);
369                    attemptClaimDrag();
370                }
371                break;
372
373            case MotionEvent.ACTION_MOVE:
374                if (mIsDragging) {
375                    trackTouchEvent(event);
376                } else {
377                    final float x = event.getX();
378                    if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
379                        setPressed(true);
380                        if (mThumb != null) {
381                            invalidate(mThumb.getBounds()); // This may be within the padding region
382                        }
383                        onStartTrackingTouch();
384                        trackTouchEvent(event);
385                        attemptClaimDrag();
386                    }
387                }
388                break;
389
390            case MotionEvent.ACTION_UP:
391                if (mIsDragging) {
392                    trackTouchEvent(event);
393                    onStopTrackingTouch();
394                    setPressed(false);
395                } else {
396                    // Touch up when we never crossed the touch slop threshold should
397                    // be interpreted as a tap-seek to that location.
398                    onStartTrackingTouch();
399                    trackTouchEvent(event);
400                    onStopTrackingTouch();
401                }
402                // ProgressBar doesn't know to repaint the thumb drawable
403                // in its inactive state when the touch stops (because the
404                // value has not apparently changed)
405                invalidate();
406                break;
407
408            case MotionEvent.ACTION_CANCEL:
409                if (mIsDragging) {
410                    onStopTrackingTouch();
411                    setPressed(false);
412                }
413                invalidate(); // see above explanation
414                break;
415        }
416        return true;
417    }
418
419    private void trackTouchEvent(MotionEvent event) {
420        final int width = getWidth();
421        final int available = width - mPaddingLeft - mPaddingRight;
422        int x = (int)event.getX();
423        float scale;
424        float progress = 0;
425        if (isLayoutRtl()) {
426            if (x > width - mPaddingRight) {
427                scale = 0.0f;
428            } else if (x < mPaddingLeft) {
429                scale = 1.0f;
430            } else {
431                scale = (float)(available - x + mPaddingLeft) / (float)available;
432                progress = mTouchProgressOffset;
433            }
434        } else {
435            if (x < mPaddingLeft) {
436                scale = 0.0f;
437            } else if (x > width - mPaddingRight) {
438                scale = 1.0f;
439            } else {
440                scale = (float)(x - mPaddingLeft) / (float)available;
441                progress = mTouchProgressOffset;
442            }
443        }
444        final int max = getMax();
445        progress += scale * max;
446
447        setProgress((int) progress, true);
448    }
449
450    /**
451     * Tries to claim the user's drag motion, and requests disallowing any
452     * ancestors from stealing events in the drag.
453     */
454    private void attemptClaimDrag() {
455        if (mParent != null) {
456            mParent.requestDisallowInterceptTouchEvent(true);
457        }
458    }
459
460    /**
461     * This is called when the user has started touching this widget.
462     */
463    void onStartTrackingTouch() {
464        mIsDragging = true;
465    }
466
467    /**
468     * This is called when the user either releases his touch or the touch is
469     * canceled.
470     */
471    void onStopTrackingTouch() {
472        mIsDragging = false;
473    }
474
475    /**
476     * Called when the user changes the seekbar's progress by using a key event.
477     */
478    void onKeyChange() {
479    }
480
481    @Override
482    public boolean onKeyDown(int keyCode, KeyEvent event) {
483        if (isEnabled()) {
484            int progress = getProgress();
485            switch (keyCode) {
486                case KeyEvent.KEYCODE_DPAD_LEFT:
487                    if (progress <= 0) break;
488                    setProgress(progress - mKeyProgressIncrement, true);
489                    onKeyChange();
490                    return true;
491
492                case KeyEvent.KEYCODE_DPAD_RIGHT:
493                    if (progress >= getMax()) break;
494                    setProgress(progress + mKeyProgressIncrement, true);
495                    onKeyChange();
496                    return true;
497            }
498        }
499
500        return super.onKeyDown(keyCode, event);
501    }
502
503    @Override
504    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
505        super.onInitializeAccessibilityEvent(event);
506        event.setClassName(AbsSeekBar.class.getName());
507    }
508
509    @Override
510    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
511        super.onInitializeAccessibilityNodeInfo(info);
512        info.setClassName(AbsSeekBar.class.getName());
513
514        if (isEnabled()) {
515            final int progress = getProgress();
516            if (progress > 0) {
517                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
518            }
519            if (progress < getMax()) {
520                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
521            }
522        }
523    }
524
525    @Override
526    public boolean performAccessibilityAction(int action, Bundle arguments) {
527        if (super.performAccessibilityAction(action, arguments)) {
528            return true;
529        }
530        if (!isEnabled()) {
531            return false;
532        }
533        final int progress = getProgress();
534        final int increment = Math.max(1, Math.round((float) getMax() / 5));
535        switch (action) {
536            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
537                if (progress <= 0) {
538                    return false;
539                }
540                setProgress(progress - increment, true);
541                onKeyChange();
542                return true;
543            }
544            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
545                if (progress >= getMax()) {
546                    return false;
547                }
548                setProgress(progress + increment, true);
549                onKeyChange();
550                return true;
551            }
552        }
553        return false;
554    }
555}
556