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