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