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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.ColorStateList;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.Insets;
26 import android.graphics.PorterDuff;
27 import android.graphics.Rect;
28 import android.graphics.Region.Op;
29 import android.graphics.drawable.Drawable;
30 import android.os.Bundle;
31 import android.util.AttributeSet;
32 import android.view.KeyEvent;
33 import android.view.MotionEvent;
34 import android.view.ViewConfiguration;
35 import android.view.accessibility.AccessibilityNodeInfo;
36 
37 import com.android.internal.R;
38 
39 
40 /**
41  * AbsSeekBar extends the capabilities of ProgressBar by adding a draggable thumb.
42  */
43 public abstract class AbsSeekBar extends ProgressBar {
44     private final Rect mTempRect = new Rect();
45 
46     private Drawable mThumb;
47     private ColorStateList mThumbTintList = null;
48     private PorterDuff.Mode mThumbTintMode = null;
49     private boolean mHasThumbTint = false;
50     private boolean mHasThumbTintMode = false;
51 
52     private Drawable mTickMark;
53     private ColorStateList mTickMarkTintList = null;
54     private PorterDuff.Mode mTickMarkTintMode = null;
55     private boolean mHasTickMarkTint = false;
56     private boolean mHasTickMarkTintMode = false;
57 
58     private int mThumbOffset;
59     private boolean mSplitTrack;
60 
61     /**
62      * On touch, this offset plus the scaled value from the position of the
63      * touch will form the progress value. Usually 0.
64      */
65     float mTouchProgressOffset;
66 
67     /**
68      * Whether this is user seekable.
69      */
70     boolean mIsUserSeekable = true;
71 
72     /**
73      * On key presses (right or left), the amount to increment/decrement the
74      * progress.
75      */
76     private int mKeyProgressIncrement = 1;
77 
78     private static final int NO_ALPHA = 0xFF;
79     private float mDisabledAlpha;
80 
81     private int mScaledTouchSlop;
82     private float mTouchDownX;
83     private boolean mIsDragging;
84 
AbsSeekBar(Context context)85     public AbsSeekBar(Context context) {
86         super(context);
87     }
88 
AbsSeekBar(Context context, AttributeSet attrs)89     public AbsSeekBar(Context context, AttributeSet attrs) {
90         super(context, attrs);
91     }
92 
AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr)93     public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
94         this(context, attrs, defStyleAttr, 0);
95     }
96 
AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)97     public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
98         super(context, attrs, defStyleAttr, defStyleRes);
99 
100         final TypedArray a = context.obtainStyledAttributes(
101                 attrs, R.styleable.SeekBar, defStyleAttr, defStyleRes);
102 
103         final Drawable thumb = a.getDrawable(R.styleable.SeekBar_thumb);
104         setThumb(thumb);
105 
106         if (a.hasValue(R.styleable.SeekBar_thumbTintMode)) {
107             mThumbTintMode = Drawable.parseTintMode(a.getInt(
108                     R.styleable.SeekBar_thumbTintMode, -1), mThumbTintMode);
109             mHasThumbTintMode = true;
110         }
111 
112         if (a.hasValue(R.styleable.SeekBar_thumbTint)) {
113             mThumbTintList = a.getColorStateList(R.styleable.SeekBar_thumbTint);
114             mHasThumbTint = true;
115         }
116 
117         final Drawable tickMark = a.getDrawable(R.styleable.SeekBar_tickMark);
118         setTickMark(tickMark);
119 
120         if (a.hasValue(R.styleable.SeekBar_tickMarkTintMode)) {
121             mTickMarkTintMode = Drawable.parseTintMode(a.getInt(
122                     R.styleable.SeekBar_tickMarkTintMode, -1), mTickMarkTintMode);
123             mHasTickMarkTintMode = true;
124         }
125 
126         if (a.hasValue(R.styleable.SeekBar_tickMarkTint)) {
127             mTickMarkTintList = a.getColorStateList(R.styleable.SeekBar_tickMarkTint);
128             mHasTickMarkTint = true;
129         }
130 
131         mSplitTrack = a.getBoolean(R.styleable.SeekBar_splitTrack, false);
132 
133         // Guess thumb offset if thumb != null, but allow layout to override.
134         final int thumbOffset = a.getDimensionPixelOffset(
135                 R.styleable.SeekBar_thumbOffset, getThumbOffset());
136         setThumbOffset(thumbOffset);
137 
138         final boolean useDisabledAlpha = a.getBoolean(R.styleable.SeekBar_useDisabledAlpha, true);
139         a.recycle();
140 
141         if (useDisabledAlpha) {
142             final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Theme, 0, 0);
143             mDisabledAlpha = ta.getFloat(R.styleable.Theme_disabledAlpha, 0.5f);
144             ta.recycle();
145         } else {
146             mDisabledAlpha = 1.0f;
147         }
148 
149         applyThumbTint();
150         applyTickMarkTint();
151 
152         mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
153     }
154 
155     /**
156      * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
157      * <p>
158      * If the thumb is a valid drawable (i.e. not null), half its width will be
159      * used as the new thumb offset (@see #setThumbOffset(int)).
160      *
161      * @param thumb Drawable representing the thumb
162      */
setThumb(Drawable thumb)163     public void setThumb(Drawable thumb) {
164         final boolean needUpdate;
165         // This way, calling setThumb again with the same bitmap will result in
166         // it recalcuating mThumbOffset (if for example it the bounds of the
167         // drawable changed)
168         if (mThumb != null && thumb != mThumb) {
169             mThumb.setCallback(null);
170             needUpdate = true;
171         } else {
172             needUpdate = false;
173         }
174 
175         if (thumb != null) {
176             thumb.setCallback(this);
177             if (canResolveLayoutDirection()) {
178                 thumb.setLayoutDirection(getLayoutDirection());
179             }
180 
181             // Assuming the thumb drawable is symmetric, set the thumb offset
182             // such that the thumb will hang halfway off either edge of the
183             // progress bar.
184             mThumbOffset = thumb.getIntrinsicWidth() / 2;
185 
186             // If we're updating get the new states
187             if (needUpdate &&
188                     (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()
189                         || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {
190                 requestLayout();
191             }
192         }
193 
194         mThumb = thumb;
195 
196         applyThumbTint();
197         invalidate();
198 
199         if (needUpdate) {
200             updateThumbAndTrackPos(getWidth(), getHeight());
201             if (thumb != null && thumb.isStateful()) {
202                 // Note that if the states are different this won't work.
203                 // For now, let's consider that an app bug.
204                 int[] state = getDrawableState();
205                 thumb.setState(state);
206             }
207         }
208     }
209 
210     /**
211      * Return the drawable used to represent the scroll thumb - the component that
212      * the user can drag back and forth indicating the current value by its position.
213      *
214      * @return The current thumb drawable
215      */
getThumb()216     public Drawable getThumb() {
217         return mThumb;
218     }
219 
220     /**
221      * Applies a tint to the thumb drawable. Does not modify the current tint
222      * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
223      * <p>
224      * Subsequent calls to {@link #setThumb(Drawable)} will automatically
225      * mutate the drawable and apply the specified tint and tint mode using
226      * {@link Drawable#setTintList(ColorStateList)}.
227      *
228      * @param tint the tint to apply, may be {@code null} to clear tint
229      *
230      * @attr ref android.R.styleable#SeekBar_thumbTint
231      * @see #getThumbTintList()
232      * @see Drawable#setTintList(ColorStateList)
233      */
setThumbTintList(@ullable ColorStateList tint)234     public void setThumbTintList(@Nullable ColorStateList tint) {
235         mThumbTintList = tint;
236         mHasThumbTint = true;
237 
238         applyThumbTint();
239     }
240 
241     /**
242      * Returns the tint applied to the thumb drawable, if specified.
243      *
244      * @return the tint applied to the thumb drawable
245      * @attr ref android.R.styleable#SeekBar_thumbTint
246      * @see #setThumbTintList(ColorStateList)
247      */
248     @Nullable
getThumbTintList()249     public ColorStateList getThumbTintList() {
250         return mThumbTintList;
251     }
252 
253     /**
254      * Specifies the blending mode used to apply the tint specified by
255      * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. The
256      * default mode is {@link PorterDuff.Mode#SRC_IN}.
257      *
258      * @param tintMode the blending mode used to apply the tint, may be
259      *                 {@code null} to clear tint
260      *
261      * @attr ref android.R.styleable#SeekBar_thumbTintMode
262      * @see #getThumbTintMode()
263      * @see Drawable#setTintMode(PorterDuff.Mode)
264      */
setThumbTintMode(@ullable PorterDuff.Mode tintMode)265     public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
266         mThumbTintMode = tintMode;
267         mHasThumbTintMode = true;
268 
269         applyThumbTint();
270     }
271 
272     /**
273      * Returns the blending mode used to apply the tint to the thumb drawable,
274      * if specified.
275      *
276      * @return the blending mode used to apply the tint to the thumb drawable
277      * @attr ref android.R.styleable#SeekBar_thumbTintMode
278      * @see #setThumbTintMode(PorterDuff.Mode)
279      */
280     @Nullable
getThumbTintMode()281     public PorterDuff.Mode getThumbTintMode() {
282         return mThumbTintMode;
283     }
284 
applyThumbTint()285     private void applyThumbTint() {
286         if (mThumb != null && (mHasThumbTint || mHasThumbTintMode)) {
287             mThumb = mThumb.mutate();
288 
289             if (mHasThumbTint) {
290                 mThumb.setTintList(mThumbTintList);
291             }
292 
293             if (mHasThumbTintMode) {
294                 mThumb.setTintMode(mThumbTintMode);
295             }
296 
297             // The drawable (or one of its children) may not have been
298             // stateful before applying the tint, so let's try again.
299             if (mThumb.isStateful()) {
300                 mThumb.setState(getDrawableState());
301             }
302         }
303     }
304 
305     /**
306      * @see #setThumbOffset(int)
307      */
getThumbOffset()308     public int getThumbOffset() {
309         return mThumbOffset;
310     }
311 
312     /**
313      * Sets the thumb offset that allows the thumb to extend out of the range of
314      * the track.
315      *
316      * @param thumbOffset The offset amount in pixels.
317      */
setThumbOffset(int thumbOffset)318     public void setThumbOffset(int thumbOffset) {
319         mThumbOffset = thumbOffset;
320         invalidate();
321     }
322 
323     /**
324      * Specifies whether the track should be split by the thumb. When true,
325      * the thumb's optical bounds will be clipped out of the track drawable,
326      * then the thumb will be drawn into the resulting gap.
327      *
328      * @param splitTrack Whether the track should be split by the thumb
329      */
setSplitTrack(boolean splitTrack)330     public void setSplitTrack(boolean splitTrack) {
331         mSplitTrack = splitTrack;
332         invalidate();
333     }
334 
335     /**
336      * Returns whether the track should be split by the thumb.
337      */
getSplitTrack()338     public boolean getSplitTrack() {
339         return mSplitTrack;
340     }
341 
342     /**
343      * Sets the drawable displayed at each progress position, e.g. at each
344      * possible thumb position.
345      *
346      * @param tickMark the drawable to display at each progress position
347      */
setTickMark(Drawable tickMark)348     public void setTickMark(Drawable tickMark) {
349         if (mTickMark != null) {
350             mTickMark.setCallback(null);
351         }
352 
353         mTickMark = tickMark;
354 
355         if (tickMark != null) {
356             tickMark.setCallback(this);
357             tickMark.setLayoutDirection(getLayoutDirection());
358             if (tickMark.isStateful()) {
359                 tickMark.setState(getDrawableState());
360             }
361             applyTickMarkTint();
362         }
363 
364         invalidate();
365     }
366 
367     /**
368      * @return the drawable displayed at each progress position
369      */
getTickMark()370     public Drawable getTickMark() {
371         return mTickMark;
372     }
373 
374     /**
375      * Applies a tint to the tick mark drawable. Does not modify the current tint
376      * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
377      * <p>
378      * Subsequent calls to {@link #setTickMark(Drawable)} will automatically
379      * mutate the drawable and apply the specified tint and tint mode using
380      * {@link Drawable#setTintList(ColorStateList)}.
381      *
382      * @param tint the tint to apply, may be {@code null} to clear tint
383      *
384      * @attr ref android.R.styleable#SeekBar_tickMarkTint
385      * @see #getTickMarkTintList()
386      * @see Drawable#setTintList(ColorStateList)
387      */
setTickMarkTintList(@ullable ColorStateList tint)388     public void setTickMarkTintList(@Nullable ColorStateList tint) {
389         mTickMarkTintList = tint;
390         mHasTickMarkTint = true;
391 
392         applyTickMarkTint();
393     }
394 
395     /**
396      * Returns the tint applied to the tick mark drawable, if specified.
397      *
398      * @return the tint applied to the tick mark drawable
399      * @attr ref android.R.styleable#SeekBar_tickMarkTint
400      * @see #setTickMarkTintList(ColorStateList)
401      */
402     @Nullable
getTickMarkTintList()403     public ColorStateList getTickMarkTintList() {
404         return mTickMarkTintList;
405     }
406 
407     /**
408      * Specifies the blending mode used to apply the tint specified by
409      * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The
410      * default mode is {@link PorterDuff.Mode#SRC_IN}.
411      *
412      * @param tintMode the blending mode used to apply the tint, may be
413      *                 {@code null} to clear tint
414      *
415      * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
416      * @see #getTickMarkTintMode()
417      * @see Drawable#setTintMode(PorterDuff.Mode)
418      */
setTickMarkTintMode(@ullable PorterDuff.Mode tintMode)419     public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
420         mTickMarkTintMode = tintMode;
421         mHasTickMarkTintMode = true;
422 
423         applyTickMarkTint();
424     }
425 
426     /**
427      * Returns the blending mode used to apply the tint to the tick mark drawable,
428      * if specified.
429      *
430      * @return the blending mode used to apply the tint to the tick mark drawable
431      * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
432      * @see #setTickMarkTintMode(PorterDuff.Mode)
433      */
434     @Nullable
getTickMarkTintMode()435     public PorterDuff.Mode getTickMarkTintMode() {
436         return mTickMarkTintMode;
437     }
438 
applyTickMarkTint()439     private void applyTickMarkTint() {
440         if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkTintMode)) {
441             mTickMark = mTickMark.mutate();
442 
443             if (mHasTickMarkTint) {
444                 mTickMark.setTintList(mTickMarkTintList);
445             }
446 
447             if (mHasTickMarkTintMode) {
448                 mTickMark.setTintMode(mTickMarkTintMode);
449             }
450 
451             // The drawable (or one of its children) may not have been
452             // stateful before applying the tint, so let's try again.
453             if (mTickMark.isStateful()) {
454                 mTickMark.setState(getDrawableState());
455             }
456         }
457     }
458 
459     /**
460      * Sets the amount of progress changed via the arrow keys.
461      *
462      * @param increment The amount to increment or decrement when the user
463      *            presses the arrow keys.
464      */
setKeyProgressIncrement(int increment)465     public void setKeyProgressIncrement(int increment) {
466         mKeyProgressIncrement = increment < 0 ? -increment : increment;
467     }
468 
469     /**
470      * Returns the amount of progress changed via the arrow keys.
471      * <p>
472      * By default, this will be a value that is derived from the progress range.
473      *
474      * @return The amount to increment or decrement when the user presses the
475      *         arrow keys. This will be positive.
476      */
477     public int getKeyProgressIncrement() {
478         return mKeyProgressIncrement;
479     }
480 
481     @Override
482     public synchronized void setMin(int min) {
483         super.setMin(min);
484         int range = getMax() - getMin();
485 
486         if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
487 
488             // It will take the user too long to change this via keys, change it
489             // to something more reasonable
490             setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20)));
491         }
492     }
493 
494     @Override
495     public synchronized void setMax(int max) {
496         super.setMax(max);
497         int range = getMax() - getMin();
498 
499         if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
500             // It will take the user too long to change this via keys, change it
501             // to something more reasonable
502             setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20)));
503         }
504     }
505 
506     @Override
507     protected boolean verifyDrawable(@NonNull Drawable who) {
508         return who == mThumb || who == mTickMark || super.verifyDrawable(who);
509     }
510 
511     @Override
512     public void jumpDrawablesToCurrentState() {
513         super.jumpDrawablesToCurrentState();
514 
515         if (mThumb != null) {
516             mThumb.jumpToCurrentState();
517         }
518 
519         if (mTickMark != null) {
520             mTickMark.jumpToCurrentState();
521         }
522     }
523 
524     @Override
525     protected void drawableStateChanged() {
526         super.drawableStateChanged();
527 
528         final Drawable progressDrawable = getProgressDrawable();
529         if (progressDrawable != null && mDisabledAlpha < 1.0f) {
530             progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
531         }
532 
533         final Drawable thumb = mThumb;
534         if (thumb != null && thumb.isStateful()
535                 && thumb.setState(getDrawableState())) {
536             invalidateDrawable(thumb);
537         }
538 
539         final Drawable tickMark = mTickMark;
540         if (tickMark != null && tickMark.isStateful()
541                 && tickMark.setState(getDrawableState())) {
542             invalidateDrawable(tickMark);
543         }
544     }
545 
546     @Override
547     public void drawableHotspotChanged(float x, float y) {
548         super.drawableHotspotChanged(x, y);
549 
550         if (mThumb != null) {
551             mThumb.setHotspot(x, y);
552         }
553     }
554 
555     @Override
556     void onVisualProgressChanged(int id, float scale) {
557         super.onVisualProgressChanged(id, scale);
558 
559         if (id == R.id.progress) {
560             final Drawable thumb = mThumb;
561             if (thumb != null) {
562                 setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE);
563 
564                 // Since we draw translated, the drawable's bounds that it signals
565                 // for invalidation won't be the actual bounds we want invalidated,
566                 // so just invalidate this whole view.
567                 invalidate();
568             }
569         }
570     }
571 
572     @Override
573     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
574         super.onSizeChanged(w, h, oldw, oldh);
575 
576         updateThumbAndTrackPos(w, h);
577     }
578 
579     private void updateThumbAndTrackPos(int w, int h) {
580         final int paddedHeight = h - mPaddingTop - mPaddingBottom;
581         final Drawable track = getCurrentDrawable();
582         final Drawable thumb = mThumb;
583 
584         // The max height does not incorporate padding, whereas the height
585         // parameter does.
586         final int trackHeight = Math.min(mMaxHeight, paddedHeight);
587         final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
588 
589         // Apply offset to whichever item is taller.
590         final int trackOffset;
591         final int thumbOffset;
592         if (thumbHeight > trackHeight) {
593             final int offsetHeight = (paddedHeight - thumbHeight) / 2;
594             trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2;
595             thumbOffset = offsetHeight;
596         } else {
597             final int offsetHeight = (paddedHeight - trackHeight) / 2;
598             trackOffset = offsetHeight;
599             thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2;
600         }
601 
602         if (track != null) {
603             final int trackWidth = w - mPaddingRight - mPaddingLeft;
604             track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight);
605         }
606 
607         if (thumb != null) {
608             setThumbPos(w, thumb, getScale(), thumbOffset);
609         }
610     }
611 
612     private float getScale() {
613         int min = getMin();
614         int max = getMax();
615         int range = max - min;
616         return range > 0 ? (getProgress() - min) / (float) range : 0;
617     }
618 
619     /**
620      * Updates the thumb drawable bounds.
621      *
622      * @param w Width of the view, including padding
623      * @param thumb Drawable used for the thumb
624      * @param scale Current progress between 0 and 1
625      * @param offset Vertical offset for centering. If set to
626      *            {@link Integer#MIN_VALUE}, the current offset will be used.
627      */
628     private void setThumbPos(int w, Drawable thumb, float scale, int offset) {
629         int available = w - mPaddingLeft - mPaddingRight;
630         final int thumbWidth = thumb.getIntrinsicWidth();
631         final int thumbHeight = thumb.getIntrinsicHeight();
632         available -= thumbWidth;
633 
634         // The extra space for the thumb to move on the track
635         available += mThumbOffset * 2;
636 
637         final int thumbPos = (int) (scale * available + 0.5f);
638 
639         final int top, bottom;
640         if (offset == Integer.MIN_VALUE) {
641             final Rect oldBounds = thumb.getBounds();
642             top = oldBounds.top;
643             bottom = oldBounds.bottom;
644         } else {
645             top = offset;
646             bottom = offset + thumbHeight;
647         }
648 
649         final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos;
650         final int right = left + thumbWidth;
651 
652         final Drawable background = getBackground();
653         if (background != null) {
654             final int offsetX = mPaddingLeft - mThumbOffset;
655             final int offsetY = mPaddingTop;
656             background.setHotspotBounds(left + offsetX, top + offsetY,
657                     right + offsetX, bottom + offsetY);
658         }
659 
660         // Canvas will be translated, so 0,0 is where we start drawing
661         thumb.setBounds(left, top, right, bottom);
662     }
663 
664     /**
665      * @hide
666      */
667     @Override
668     public void onResolveDrawables(int layoutDirection) {
669         super.onResolveDrawables(layoutDirection);
670 
671         if (mThumb != null) {
672             mThumb.setLayoutDirection(layoutDirection);
673         }
674     }
675 
676     @Override
677     protected synchronized void onDraw(Canvas canvas) {
678         super.onDraw(canvas);
679         drawThumb(canvas);
680     }
681 
682     @Override
683     void drawTrack(Canvas canvas) {
684         final Drawable thumbDrawable = mThumb;
685         if (thumbDrawable != null && mSplitTrack) {
686             final Insets insets = thumbDrawable.getOpticalInsets();
687             final Rect tempRect = mTempRect;
688             thumbDrawable.copyBounds(tempRect);
689             tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop);
690             tempRect.left += insets.left;
691             tempRect.right -= insets.right;
692 
693             final int saveCount = canvas.save();
694             canvas.clipRect(tempRect, Op.DIFFERENCE);
695             super.drawTrack(canvas);
696             drawTickMarks(canvas);
697             canvas.restoreToCount(saveCount);
698         } else {
699             super.drawTrack(canvas);
700             drawTickMarks(canvas);
701         }
702     }
703 
704     /**
705      * @hide
706      */
707     protected void drawTickMarks(Canvas canvas) {
708         if (mTickMark != null) {
709             final int count = getMax() - getMin();
710             if (count > 1) {
711                 final int w = mTickMark.getIntrinsicWidth();
712                 final int h = mTickMark.getIntrinsicHeight();
713                 final int halfW = w >= 0 ? w / 2 : 1;
714                 final int halfH = h >= 0 ? h / 2 : 1;
715                 mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
716 
717                 final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count;
718                 final int saveCount = canvas.save();
719                 canvas.translate(mPaddingLeft, getHeight() / 2);
720                 for (int i = 0; i <= count; i++) {
721                     mTickMark.draw(canvas);
722                     canvas.translate(spacing, 0);
723                 }
724                 canvas.restoreToCount(saveCount);
725             }
726         }
727     }
728 
729     /**
730      * Draw the thumb.
731      */
732     void drawThumb(Canvas canvas) {
733         if (mThumb != null) {
734             final int saveCount = canvas.save();
735             // Translate the padding. For the x, we need to allow the thumb to
736             // draw in its extra space
737             canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
738             mThumb.draw(canvas);
739             canvas.restoreToCount(saveCount);
740         }
741     }
742 
743     @Override
744     protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
745         Drawable d = getCurrentDrawable();
746 
747         int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
748         int dw = 0;
749         int dh = 0;
750         if (d != null) {
751             dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
752             dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
753             dh = Math.max(thumbHeight, dh);
754         }
755         dw += mPaddingLeft + mPaddingRight;
756         dh += mPaddingTop + mPaddingBottom;
757 
758         setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
759                 resolveSizeAndState(dh, heightMeasureSpec, 0));
760     }
761 
762     @Override
763     public boolean onTouchEvent(MotionEvent event) {
764         if (!mIsUserSeekable || !isEnabled()) {
765             return false;
766         }
767 
768         switch (event.getAction()) {
769             case MotionEvent.ACTION_DOWN:
770                 if (isInScrollingContainer()) {
771                     mTouchDownX = event.getX();
772                 } else {
773                     startDrag(event);
774                 }
775                 break;
776 
777             case MotionEvent.ACTION_MOVE:
778                 if (mIsDragging) {
779                     trackTouchEvent(event);
780                 } else {
781                     final float x = event.getX();
782                     if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
783                         startDrag(event);
784                     }
785                 }
786                 break;
787 
788             case MotionEvent.ACTION_UP:
789                 if (mIsDragging) {
790                     trackTouchEvent(event);
791                     onStopTrackingTouch();
792                     setPressed(false);
793                 } else {
794                     // Touch up when we never crossed the touch slop threshold should
795                     // be interpreted as a tap-seek to that location.
796                     onStartTrackingTouch();
797                     trackTouchEvent(event);
798                     onStopTrackingTouch();
799                 }
800                 // ProgressBar doesn't know to repaint the thumb drawable
801                 // in its inactive state when the touch stops (because the
802                 // value has not apparently changed)
803                 invalidate();
804                 break;
805 
806             case MotionEvent.ACTION_CANCEL:
807                 if (mIsDragging) {
808                     onStopTrackingTouch();
809                     setPressed(false);
810                 }
811                 invalidate(); // see above explanation
812                 break;
813         }
814         return true;
815     }
816 
817     private void startDrag(MotionEvent event) {
818         setPressed(true);
819 
820         if (mThumb != null) {
821             // This may be within the padding region.
822             invalidate(mThumb.getBounds());
823         }
824 
825         onStartTrackingTouch();
826         trackTouchEvent(event);
827         attemptClaimDrag();
828     }
829 
830     private void setHotspot(float x, float y) {
831         final Drawable bg = getBackground();
832         if (bg != null) {
833             bg.setHotspot(x, y);
834         }
835     }
836 
837     private void trackTouchEvent(MotionEvent event) {
838         final int x = Math.round(event.getX());
839         final int y = Math.round(event.getY());
840         final int width = getWidth();
841         final int availableWidth = width - mPaddingLeft - mPaddingRight;
842 
843         final float scale;
844         float progress = 0.0f;
845         if (isLayoutRtl() && mMirrorForRtl) {
846             if (x > width - mPaddingRight) {
847                 scale = 0.0f;
848             } else if (x < mPaddingLeft) {
849                 scale = 1.0f;
850             } else {
851                 scale = (availableWidth - x + mPaddingLeft) / (float) availableWidth;
852                 progress = mTouchProgressOffset;
853             }
854         } else {
855             if (x < mPaddingLeft) {
856                 scale = 0.0f;
857             } else if (x > width - mPaddingRight) {
858                 scale = 1.0f;
859             } else {
860                 scale = (x - mPaddingLeft) / (float) availableWidth;
861                 progress = mTouchProgressOffset;
862             }
863         }
864 
865         final int range = getMax() - getMin();
866         progress += scale * range + getMin();
867 
868         setHotspot(x, y);
869         setProgressInternal(Math.round(progress), true, false);
870     }
871 
872     /**
873      * Tries to claim the user's drag motion, and requests disallowing any
874      * ancestors from stealing events in the drag.
875      */
876     private void attemptClaimDrag() {
877         if (mParent != null) {
878             mParent.requestDisallowInterceptTouchEvent(true);
879         }
880     }
881 
882     /**
883      * This is called when the user has started touching this widget.
884      */
885     void onStartTrackingTouch() {
886         mIsDragging = true;
887     }
888 
889     /**
890      * This is called when the user either releases his touch or the touch is
891      * canceled.
892      */
893     void onStopTrackingTouch() {
894         mIsDragging = false;
895     }
896 
897     /**
898      * Called when the user changes the seekbar's progress by using a key event.
899      */
900     void onKeyChange() {
901     }
902 
903     @Override
904     public boolean onKeyDown(int keyCode, KeyEvent event) {
905         if (isEnabled()) {
906             int increment = mKeyProgressIncrement;
907             switch (keyCode) {
908                 case KeyEvent.KEYCODE_DPAD_LEFT:
909                 case KeyEvent.KEYCODE_MINUS:
910                     increment = -increment;
911                     // fallthrough
912                 case KeyEvent.KEYCODE_DPAD_RIGHT:
913                 case KeyEvent.KEYCODE_PLUS:
914                 case KeyEvent.KEYCODE_EQUALS:
915                     increment = isLayoutRtl() ? -increment : increment;
916 
917                     if (setProgressInternal(getProgress() + increment, true, true)) {
918                         onKeyChange();
919                         return true;
920                     }
921                     break;
922             }
923         }
924 
925         return super.onKeyDown(keyCode, event);
926     }
927 
928     @Override
929     public CharSequence getAccessibilityClassName() {
930         return AbsSeekBar.class.getName();
931     }
932 
933     /** @hide */
934     @Override
935     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
936         super.onInitializeAccessibilityNodeInfoInternal(info);
937 
938         if (isEnabled()) {
939             final int progress = getProgress();
940             if (progress > getMin()) {
941                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
942             }
943             if (progress < getMax()) {
944                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
945             }
946         }
947     }
948 
949     /** @hide */
950     @Override
951     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
952         if (super.performAccessibilityActionInternal(action, arguments)) {
953             return true;
954         }
955 
956         if (!isEnabled()) {
957             return false;
958         }
959 
960         switch (action) {
961             case R.id.accessibilityActionSetProgress: {
962                 if (!canUserSetProgress()) {
963                     return false;
964                 }
965                 if (arguments == null || !arguments.containsKey(
966                         AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
967                     return false;
968                 }
969                 float value = arguments.getFloat(
970                         AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE);
971                 return setProgressInternal((int) value, true, true);
972             }
973             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
974             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
975                 if (!canUserSetProgress()) {
976                     return false;
977                 }
978                 int range = getMax() - getMin();
979                 int increment = Math.max(1, Math.round((float) range / 20));
980                 if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
981                     increment = -increment;
982                 }
983 
984                 // Let progress bar handle clamping values.
985                 if (setProgressInternal(getProgress() + increment, true, true)) {
986                     onKeyChange();
987                     return true;
988                 }
989                 return false;
990             }
991         }
992         return false;
993     }
994 
995     /**
996      * @return whether user can change progress on the view
997      */
998     boolean canUserSetProgress() {
999         return !isIndeterminate() && isEnabled();
1000     }
1001 
1002     @Override
1003     public void onRtlPropertiesChanged(int layoutDirection) {
1004         super.onRtlPropertiesChanged(layoutDirection);
1005 
1006         final Drawable thumb = mThumb;
1007         if (thumb != null) {
1008             setThumbPos(getWidth(), thumb, getScale(), Integer.MIN_VALUE);
1009 
1010             // Since we draw translated, the drawable's bounds that it signals
1011             // for invalidation won't be the actual bounds we want invalidated,
1012             // so just invalidate this whole view.
1013             invalidate();
1014         }
1015     }
1016 }
1017