• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.widget;
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.os.Bundle;
25 import android.util.AttributeSet;
26 import android.view.KeyEvent;
27 import android.view.MotionEvent;
28 import android.view.ViewConfiguration;
29 import android.view.accessibility.AccessibilityEvent;
30 import android.view.accessibility.AccessibilityNodeInfo;
31 
32 public 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 
AbsSeekBar(Context context)60     public AbsSeekBar(Context context) {
61         super(context);
62     }
63 
AbsSeekBar(Context context, AttributeSet attrs)64     public AbsSeekBar(Context context, AttributeSet attrs) {
65         super(context, attrs);
66     }
67 
AbsSeekBar(Context context, AttributeSet attrs, int defStyle)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      */
setThumb(Drawable thumb)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 
111             // Assuming the thumb drawable is symmetric, set the thumb offset
112             // such that the thumb will hang halfway off either edge of the
113             // progress bar.
114             mThumbOffset = thumb.getIntrinsicWidth() / 2;
115 
116             // If we're updating get the new states
117             if (needUpdate &&
118                     (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()
119                         || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {
120                 requestLayout();
121             }
122         }
123         mThumb = thumb;
124         invalidate();
125         if (needUpdate) {
126             updateThumbPos(getWidth(), getHeight());
127             if (thumb != null && thumb.isStateful()) {
128                 // Note that if the states are different this won't work.
129                 // For now, let's consider that an app bug.
130                 int[] state = getDrawableState();
131                 thumb.setState(state);
132             }
133         }
134     }
135 
136     /**
137      * Return the drawable used to represent the scroll thumb - the component that
138      * the user can drag back and forth indicating the current value by its position.
139      *
140      * @return The current thumb drawable
141      */
getThumb()142     public Drawable getThumb() {
143         return mThumb;
144     }
145 
146     /**
147      * @see #setThumbOffset(int)
148      */
getThumbOffset()149     public int getThumbOffset() {
150         return mThumbOffset;
151     }
152 
153     /**
154      * Sets the thumb offset that allows the thumb to extend out of the range of
155      * the track.
156      *
157      * @param thumbOffset The offset amount in pixels.
158      */
setThumbOffset(int thumbOffset)159     public void setThumbOffset(int thumbOffset) {
160         mThumbOffset = thumbOffset;
161         invalidate();
162     }
163 
164     /**
165      * Sets the amount of progress changed via the arrow keys.
166      *
167      * @param increment The amount to increment or decrement when the user
168      *            presses the arrow keys.
169      */
setKeyProgressIncrement(int increment)170     public void setKeyProgressIncrement(int increment) {
171         mKeyProgressIncrement = increment < 0 ? -increment : increment;
172     }
173 
174     /**
175      * Returns the amount of progress changed via the arrow keys.
176      * <p>
177      * By default, this will be a value that is derived from the max progress.
178      *
179      * @return The amount to increment or decrement when the user presses the
180      *         arrow keys. This will be positive.
181      */
182     public int getKeyProgressIncrement() {
183         return mKeyProgressIncrement;
184     }
185 
186     @Override
187     public synchronized void setMax(int max) {
188         super.setMax(max);
189 
190         if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) {
191             // It will take the user too long to change this via keys, change it
192             // to something more reasonable
193             setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20)));
194         }
195     }
196 
197     @Override
198     protected boolean verifyDrawable(Drawable who) {
199         return who == mThumb || super.verifyDrawable(who);
200     }
201 
202     @Override
203     public void jumpDrawablesToCurrentState() {
204         super.jumpDrawablesToCurrentState();
205         if (mThumb != null) mThumb.jumpToCurrentState();
206     }
207 
208     @Override
209     protected void drawableStateChanged() {
210         super.drawableStateChanged();
211 
212         Drawable progressDrawable = getProgressDrawable();
213         if (progressDrawable != null) {
214             progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
215         }
216 
217         if (mThumb != null && mThumb.isStateful()) {
218             int[] state = getDrawableState();
219             mThumb.setState(state);
220         }
221     }
222 
223     @Override
224     void onProgressRefresh(float scale, boolean fromUser) {
225         super.onProgressRefresh(scale, fromUser);
226         Drawable thumb = mThumb;
227         if (thumb != null) {
228             setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE);
229             /*
230              * Since we draw translated, the drawable's bounds that it signals
231              * for invalidation won't be the actual bounds we want invalidated,
232              * so just invalidate this whole view.
233              */
234             invalidate();
235         }
236     }
237 
238 
239     @Override
240     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
241         updateThumbPos(w, h);
242     }
243 
244     private void updateThumbPos(int w, int h) {
245         Drawable d = getCurrentDrawable();
246         Drawable thumb = mThumb;
247         int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
248         // The max height does not incorporate padding, whereas the height
249         // parameter does
250         int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
251 
252         int max = getMax();
253         float scale = max > 0 ? (float) getProgress() / (float) max : 0;
254 
255         if (thumbHeight > trackHeight) {
256             if (thumb != null) {
257                 setThumbPos(w, thumb, scale, 0);
258             }
259             int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
260             if (d != null) {
261                 // Canvas will be translated by the padding, so 0,0 is where we start drawing
262                 d.setBounds(0, gapForCenteringTrack,
263                         w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
264                         - mPaddingTop);
265             }
266         } else {
267             if (d != null) {
268                 // Canvas will be translated by the padding, so 0,0 is where we start drawing
269                 d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
270                         - mPaddingTop);
271             }
272             int gap = (trackHeight - thumbHeight) / 2;
273             if (thumb != null) {
274                 setThumbPos(w, thumb, scale, gap);
275             }
276         }
277     }
278 
279     /**
280      * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
281      */
282     private void setThumbPos(int w, Drawable thumb, float scale, int gap) {
283         int available = w - mPaddingLeft - mPaddingRight;
284         int thumbWidth = thumb.getIntrinsicWidth();
285         int thumbHeight = thumb.getIntrinsicHeight();
286         available -= thumbWidth;
287 
288         // The extra space for the thumb to move on the track
289         available += mThumbOffset * 2;
290 
291         int thumbPos = (int) (scale * available);
292 
293         int topBound, bottomBound;
294         if (gap == Integer.MIN_VALUE) {
295             Rect oldBounds = thumb.getBounds();
296             topBound = oldBounds.top;
297             bottomBound = oldBounds.bottom;
298         } else {
299             topBound = gap;
300             bottomBound = gap + thumbHeight;
301         }
302 
303         // Canvas will be translated, so 0,0 is where we start drawing
304         thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound);
305     }
306 
307     @Override
308     protected synchronized void onDraw(Canvas canvas) {
309         super.onDraw(canvas);
310         if (mThumb != null) {
311             canvas.save();
312             // Translate the padding. For the x, we need to allow the thumb to
313             // draw in its extra space
314             canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
315             mThumb.draw(canvas);
316             canvas.restore();
317         }
318     }
319 
320     @Override
321     protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
322         Drawable d = getCurrentDrawable();
323 
324         int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
325         int dw = 0;
326         int dh = 0;
327         if (d != null) {
328             dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
329             dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
330             dh = Math.max(thumbHeight, dh);
331         }
332         dw += mPaddingLeft + mPaddingRight;
333         dh += mPaddingTop + mPaddingBottom;
334 
335         setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
336                 resolveSizeAndState(dh, heightMeasureSpec, 0));
337     }
338 
339     @Override
340     public boolean onTouchEvent(MotionEvent event) {
341         if (!mIsUserSeekable || !isEnabled()) {
342             return false;
343         }
344 
345         switch (event.getAction()) {
346             case MotionEvent.ACTION_DOWN:
347                 if (isInScrollingContainer()) {
348                     mTouchDownX = event.getX();
349                 } else {
350                     setPressed(true);
351                     if (mThumb != null) {
352                         invalidate(mThumb.getBounds()); // This may be within the padding region
353                     }
354                     onStartTrackingTouch();
355                     trackTouchEvent(event);
356                     attemptClaimDrag();
357                 }
358                 break;
359 
360             case MotionEvent.ACTION_MOVE:
361                 if (mIsDragging) {
362                     trackTouchEvent(event);
363                 } else {
364                     final float x = event.getX();
365                     if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
366                         setPressed(true);
367                         if (mThumb != null) {
368                             invalidate(mThumb.getBounds()); // This may be within the padding region
369                         }
370                         onStartTrackingTouch();
371                         trackTouchEvent(event);
372                         attemptClaimDrag();
373                     }
374                 }
375                 break;
376 
377             case MotionEvent.ACTION_UP:
378                 if (mIsDragging) {
379                     trackTouchEvent(event);
380                     onStopTrackingTouch();
381                     setPressed(false);
382                 } else {
383                     // Touch up when we never crossed the touch slop threshold should
384                     // be interpreted as a tap-seek to that location.
385                     onStartTrackingTouch();
386                     trackTouchEvent(event);
387                     onStopTrackingTouch();
388                 }
389                 // ProgressBar doesn't know to repaint the thumb drawable
390                 // in its inactive state when the touch stops (because the
391                 // value has not apparently changed)
392                 invalidate();
393                 break;
394 
395             case MotionEvent.ACTION_CANCEL:
396                 if (mIsDragging) {
397                     onStopTrackingTouch();
398                     setPressed(false);
399                 }
400                 invalidate(); // see above explanation
401                 break;
402         }
403         return true;
404     }
405 
406     private void trackTouchEvent(MotionEvent event) {
407         final int width = getWidth();
408         final int available = width - mPaddingLeft - mPaddingRight;
409         int x = (int)event.getX();
410         float scale;
411         float progress = 0;
412         if (x < mPaddingLeft) {
413             scale = 0.0f;
414         } else if (x > width - mPaddingRight) {
415             scale = 1.0f;
416         } else {
417             scale = (float)(x - mPaddingLeft) / (float)available;
418             progress = mTouchProgressOffset;
419         }
420 
421         final int max = getMax();
422         progress += scale * max;
423 
424         setProgress((int) progress, true);
425     }
426 
427     /**
428      * Tries to claim the user's drag motion, and requests disallowing any
429      * ancestors from stealing events in the drag.
430      */
431     private void attemptClaimDrag() {
432         if (mParent != null) {
433             mParent.requestDisallowInterceptTouchEvent(true);
434         }
435     }
436 
437     /**
438      * This is called when the user has started touching this widget.
439      */
440     void onStartTrackingTouch() {
441         mIsDragging = true;
442     }
443 
444     /**
445      * This is called when the user either releases his touch or the touch is
446      * canceled.
447      */
448     void onStopTrackingTouch() {
449         mIsDragging = false;
450     }
451 
452     /**
453      * Called when the user changes the seekbar's progress by using a key event.
454      */
455     void onKeyChange() {
456     }
457 
458     @Override
459     public boolean onKeyDown(int keyCode, KeyEvent event) {
460         if (isEnabled()) {
461             int progress = getProgress();
462             switch (keyCode) {
463                 case KeyEvent.KEYCODE_DPAD_LEFT:
464                     if (progress <= 0) break;
465                     setProgress(progress - mKeyProgressIncrement, true);
466                     onKeyChange();
467                     return true;
468 
469                 case KeyEvent.KEYCODE_DPAD_RIGHT:
470                     if (progress >= getMax()) break;
471                     setProgress(progress + mKeyProgressIncrement, true);
472                     onKeyChange();
473                     return true;
474             }
475         }
476 
477         return super.onKeyDown(keyCode, event);
478     }
479 
480     @Override
481     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
482         super.onInitializeAccessibilityEvent(event);
483         event.setClassName(AbsSeekBar.class.getName());
484     }
485 
486     @Override
487     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
488         super.onInitializeAccessibilityNodeInfo(info);
489         info.setClassName(AbsSeekBar.class.getName());
490 
491         if (isEnabled()) {
492             final int progress = getProgress();
493             if (progress > 0) {
494                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
495             }
496             if (progress < getMax()) {
497                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
498             }
499         }
500     }
501 
502     @Override
503     public boolean performAccessibilityAction(int action, Bundle arguments) {
504         if (super.performAccessibilityAction(action, arguments)) {
505             return true;
506         }
507         if (!isEnabled()) {
508             return false;
509         }
510         final int progress = getProgress();
511         final int increment = Math.max(1, Math.round((float) getMax() / 5));
512         switch (action) {
513             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
514                 if (progress <= 0) {
515                     return false;
516                 }
517                 setProgress(progress - increment, true);
518                 onKeyChange();
519                 return true;
520             }
521             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
522                 if (progress >= getMax()) {
523                     return false;
524                 }
525                 setProgress(progress + increment, true);
526                 onKeyChange();
527                 return true;
528             }
529         }
530         return false;
531     }
532 }
533