Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.content.res.ColorStateList;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Canvas;
     25 import android.graphics.Insets;
     26 import android.graphics.PorterDuff;
     27 import android.graphics.Rect;
     28 import android.graphics.Region.Op;
     29 import android.graphics.drawable.Drawable;
     30 import android.os.Bundle;
     31 import android.util.AttributeSet;
     32 import android.view.KeyEvent;
     33 import android.view.MotionEvent;
     34 import android.view.ViewConfiguration;
     35 import android.view.accessibility.AccessibilityNodeInfo;
     36 
     37 import com.android.internal.R;
     38 
     39 
     40 /**
     41  * AbsSeekBar extends the capabilities of ProgressBar by adding a draggable thumb.
     42  */
     43 public 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();
    866         progress += scale * range + getMin();
    867 
    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 }
   1017