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