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(getLayoutDirection());
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        super.onSizeChanged(w, h, oldw, oldh);
245        updateThumbPos(w, h);
246    }
247
248    private void updateThumbPos(int w, int h) {
249        Drawable d = getCurrentDrawable();
250        Drawable thumb = mThumb;
251        int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
252        // The max height does not incorporate padding, whereas the height
253        // parameter does
254        int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
255
256        int max = getMax();
257        float scale = max > 0 ? (float) getProgress() / (float) max : 0;
258
259        if (thumbHeight > trackHeight) {
260            if (thumb != null) {
261                setThumbPos(w, thumb, scale, 0);
262            }
263            int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
264            if (d != null) {
265                // Canvas will be translated by the padding, so 0,0 is where we start drawing
266                d.setBounds(0, gapForCenteringTrack,
267                        w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
268                        - mPaddingTop);
269            }
270        } else {
271            if (d != null) {
272                // Canvas will be translated by the padding, so 0,0 is where we start drawing
273                d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
274                        - mPaddingTop);
275            }
276            int gap = (trackHeight - thumbHeight) / 2;
277            if (thumb != null) {
278                setThumbPos(w, thumb, scale, gap);
279            }
280        }
281    }
282
283    /**
284     * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
285     */
286    private void setThumbPos(int w, Drawable thumb, float scale, int gap) {
287        int available = w - mPaddingLeft - mPaddingRight;
288        int thumbWidth = thumb.getIntrinsicWidth();
289        int thumbHeight = thumb.getIntrinsicHeight();
290        available -= thumbWidth;
291
292        // The extra space for the thumb to move on the track
293        available += mThumbOffset * 2;
294
295        int thumbPos = (int) (scale * available);
296
297        int topBound, bottomBound;
298        if (gap == Integer.MIN_VALUE) {
299            Rect oldBounds = thumb.getBounds();
300            topBound = oldBounds.top;
301            bottomBound = oldBounds.bottom;
302        } else {
303            topBound = gap;
304            bottomBound = gap + thumbHeight;
305        }
306
307        // Canvas will be translated, so 0,0 is where we start drawing
308        final int left = isLayoutRtl() ? available - thumbPos : thumbPos;
309        thumb.setBounds(left, topBound, left + thumbWidth, bottomBound);
310    }
311
312    /**
313     * @hide
314     */
315    @Override
316    public void onResolveDrawables(int layoutDirection) {
317        super.onResolveDrawables(layoutDirection);
318
319        if (mThumb != null) {
320            mThumb.setLayoutDirection(layoutDirection);
321        }
322    }
323
324    @Override
325    protected synchronized void onDraw(Canvas canvas) {
326        super.onDraw(canvas);
327        if (mThumb != null) {
328            canvas.save();
329            // Translate the padding. For the x, we need to allow the thumb to
330            // draw in its extra space
331            canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
332            mThumb.draw(canvas);
333            canvas.restore();
334        }
335    }
336
337    @Override
338    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
339        Drawable d = getCurrentDrawable();
340
341        int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
342        int dw = 0;
343        int dh = 0;
344        if (d != null) {
345            dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
346            dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
347            dh = Math.max(thumbHeight, dh);
348        }
349        dw += mPaddingLeft + mPaddingRight;
350        dh += mPaddingTop + mPaddingBottom;
351
352        setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
353                resolveSizeAndState(dh, heightMeasureSpec, 0));
354    }
355
356    @Override
357    public boolean onTouchEvent(MotionEvent event) {
358        if (!mIsUserSeekable || !isEnabled()) {
359            return false;
360        }
361
362        switch (event.getAction()) {
363            case MotionEvent.ACTION_DOWN:
364                if (isInScrollingContainer()) {
365                    mTouchDownX = event.getX();
366                } else {
367                    setPressed(true);
368                    if (mThumb != null) {
369                        invalidate(mThumb.getBounds()); // This may be within the padding region
370                    }
371                    onStartTrackingTouch();
372                    trackTouchEvent(event);
373                    attemptClaimDrag();
374                }
375                break;
376
377            case MotionEvent.ACTION_MOVE:
378                if (mIsDragging) {
379                    trackTouchEvent(event);
380                } else {
381                    final float x = event.getX();
382                    if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
383                        setPressed(true);
384                        if (mThumb != null) {
385                            invalidate(mThumb.getBounds()); // This may be within the padding region
386                        }
387                        onStartTrackingTouch();
388                        trackTouchEvent(event);
389                        attemptClaimDrag();
390                    }
391                }
392                break;
393
394            case MotionEvent.ACTION_UP:
395                if (mIsDragging) {
396                    trackTouchEvent(event);
397                    onStopTrackingTouch();
398                    setPressed(false);
399                } else {
400                    // Touch up when we never crossed the touch slop threshold should
401                    // be interpreted as a tap-seek to that location.
402                    onStartTrackingTouch();
403                    trackTouchEvent(event);
404                    onStopTrackingTouch();
405                }
406                // ProgressBar doesn't know to repaint the thumb drawable
407                // in its inactive state when the touch stops (because the
408                // value has not apparently changed)
409                invalidate();
410                break;
411
412            case MotionEvent.ACTION_CANCEL:
413                if (mIsDragging) {
414                    onStopTrackingTouch();
415                    setPressed(false);
416                }
417                invalidate(); // see above explanation
418                break;
419        }
420        return true;
421    }
422
423    private void trackTouchEvent(MotionEvent event) {
424        final int width = getWidth();
425        final int available = width - mPaddingLeft - mPaddingRight;
426        int x = (int)event.getX();
427        float scale;
428        float progress = 0;
429        if (isLayoutRtl()) {
430            if (x > width - mPaddingRight) {
431                scale = 0.0f;
432            } else if (x < mPaddingLeft) {
433                scale = 1.0f;
434            } else {
435                scale = (float)(available - x + mPaddingLeft) / (float)available;
436                progress = mTouchProgressOffset;
437            }
438        } else {
439            if (x < mPaddingLeft) {
440                scale = 0.0f;
441            } else if (x > width - mPaddingRight) {
442                scale = 1.0f;
443            } else {
444                scale = (float)(x - mPaddingLeft) / (float)available;
445                progress = mTouchProgressOffset;
446            }
447        }
448        final int max = getMax();
449        progress += scale * max;
450
451        setProgress((int) progress, true);
452    }
453
454    /**
455     * Tries to claim the user's drag motion, and requests disallowing any
456     * ancestors from stealing events in the drag.
457     */
458    private void attemptClaimDrag() {
459        if (mParent != null) {
460            mParent.requestDisallowInterceptTouchEvent(true);
461        }
462    }
463
464    /**
465     * This is called when the user has started touching this widget.
466     */
467    void onStartTrackingTouch() {
468        mIsDragging = true;
469    }
470
471    /**
472     * This is called when the user either releases his touch or the touch is
473     * canceled.
474     */
475    void onStopTrackingTouch() {
476        mIsDragging = false;
477    }
478
479    /**
480     * Called when the user changes the seekbar's progress by using a key event.
481     */
482    void onKeyChange() {
483    }
484
485    @Override
486    public boolean onKeyDown(int keyCode, KeyEvent event) {
487        if (isEnabled()) {
488            int progress = getProgress();
489            switch (keyCode) {
490                case KeyEvent.KEYCODE_DPAD_LEFT:
491                    if (progress <= 0) break;
492                    setProgress(progress - mKeyProgressIncrement, true);
493                    onKeyChange();
494                    return true;
495
496                case KeyEvent.KEYCODE_DPAD_RIGHT:
497                    if (progress >= getMax()) break;
498                    setProgress(progress + mKeyProgressIncrement, true);
499                    onKeyChange();
500                    return true;
501            }
502        }
503
504        return super.onKeyDown(keyCode, event);
505    }
506
507    @Override
508    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
509        super.onInitializeAccessibilityEvent(event);
510        event.setClassName(AbsSeekBar.class.getName());
511    }
512
513    @Override
514    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
515        super.onInitializeAccessibilityNodeInfo(info);
516        info.setClassName(AbsSeekBar.class.getName());
517
518        if (isEnabled()) {
519            final int progress = getProgress();
520            if (progress > 0) {
521                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
522            }
523            if (progress < getMax()) {
524                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
525            }
526        }
527    }
528
529    @Override
530    public boolean performAccessibilityAction(int action, Bundle arguments) {
531        if (super.performAccessibilityAction(action, arguments)) {
532            return true;
533        }
534        if (!isEnabled()) {
535            return false;
536        }
537        final int progress = getProgress();
538        final int increment = Math.max(1, Math.round((float) getMax() / 5));
539        switch (action) {
540            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
541                if (progress <= 0) {
542                    return false;
543                }
544                setProgress(progress - increment, true);
545                onKeyChange();
546                return true;
547            }
548            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
549                if (progress >= getMax()) {
550                    return false;
551                }
552                setProgress(progress + increment, true);
553                onKeyChange();
554                return true;
555            }
556        }
557        return false;
558    }
559
560    @Override
561    public void onRtlPropertiesChanged(int layoutDirection) {
562        super.onRtlPropertiesChanged(layoutDirection);
563
564        int max = getMax();
565        float scale = max > 0 ? (float) getProgress() / (float) max : 0;
566
567        Drawable thumb = mThumb;
568        if (thumb != null) {
569            setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE);
570            /*
571             * Since we draw translated, the drawable's bounds that it signals
572             * for invalidation won't be the actual bounds we want invalidated,
573             * so just invalidate this whole view.
574             */
575            invalidate();
576        }
577    }
578}
579