OSDN Git Service

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