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