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