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