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