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