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