Home | History | Annotate | Download | only in seekbar
      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 com.android.musicfx.seekbar;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.graphics.Canvas;
     22 import android.graphics.Rect;
     23 import android.graphics.drawable.Drawable;
     24 import android.util.AttributeSet;
     25 import android.view.KeyEvent;
     26 import android.view.MotionEvent;
     27 
     28 public abstract class AbsSeekBar extends ProgressBar {
     29     private Drawable mThumb;
     30     private int mThumbOffset;
     31 
     32     /**
     33      * On touch, this offset plus the scaled value from the position of the
     34      * touch will form the progress value. Usually 0.
     35      */
     36     float mTouchProgressOffset;
     37 
     38     /**
     39      * Whether this is user seekable.
     40      */
     41     boolean mIsUserSeekable = true;
     42 
     43     boolean mIsVertical = false;
     44     /**
     45      * On key presses (right or left), the amount to increment/decrement the
     46      * progress.
     47      */
     48     private int mKeyProgressIncrement = 1;
     49 
     50     private static final int NO_ALPHA = 0xFF;
     51     private float mDisabledAlpha;
     52 
     53     public AbsSeekBar(Context context) {
     54         super(context);
     55     }
     56 
     57     public AbsSeekBar(Context context, AttributeSet attrs) {
     58         super(context, attrs);
     59     }
     60 
     61     public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) {
     62         super(context, attrs, defStyle);
     63 
     64         TypedArray a = context.obtainStyledAttributes(attrs,
     65                 com.android.internal.R.styleable.SeekBar, defStyle, 0);
     66         Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb);
     67         setThumb(thumb); // will guess mThumbOffset if thumb != null...
     68         // ...but allow layout to override this
     69         int thumbOffset = a.getDimensionPixelOffset(
     70                 com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset());
     71         setThumbOffset(thumbOffset);
     72         a.recycle();
     73 
     74         a = context.obtainStyledAttributes(attrs,
     75                 com.android.internal.R.styleable.Theme, 0, 0);
     76         mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);
     77         a.recycle();
     78     }
     79 
     80     /**
     81      * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
     82      * <p>
     83      * If the thumb is a valid drawable (i.e. not null), half its width will be
     84      * used as the new thumb offset (@see #setThumbOffset(int)).
     85      *
     86      * @param thumb Drawable representing the thumb
     87      */
     88     public void setThumb(Drawable thumb) {
     89         boolean needUpdate;
     90         // This way, calling setThumb again with the same bitmap will result in
     91         // it recalcuating mThumbOffset (if for example it the bounds of the
     92         // drawable changed)
     93         if (mThumb != null && thumb != mThumb) {
     94             mThumb.setCallback(null);
     95             needUpdate = true;
     96         } else {
     97             needUpdate = false;
     98         }
     99         if (thumb != null) {
    100             thumb.setCallback(this);
    101 
    102             // Assuming the thumb drawable is symmetric, set the thumb offset
    103             // such that the thumb will hang halfway off either edge of the
    104             // progress bar.
    105             if (mIsVertical) {
    106                 mThumbOffset = thumb.getIntrinsicHeight() / 2;
    107             } else {
    108                 mThumbOffset = thumb.getIntrinsicWidth() / 2;
    109             }
    110 
    111             // If we're updating get the new states
    112             if (needUpdate &&
    113                     (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()
    114                         || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {
    115                 requestLayout();
    116             }
    117         }
    118         mThumb = thumb;
    119         invalidate();
    120         if (needUpdate) {
    121             updateThumbPos(getWidth(), getHeight());
    122             if (thumb.isStateful()) {
    123                 // Note that if the states are different this won't work.
    124                 // For now, let's consider that an app bug.
    125                 int[] state = getDrawableState();
    126                 thumb.setState(state);
    127             }
    128         }
    129     }
    130 
    131     /**
    132      * @see #setThumbOffset(int)
    133      */
    134     public int getThumbOffset() {
    135         return mThumbOffset;
    136     }
    137 
    138     /**
    139      * Sets the thumb offset that allows the thumb to extend out of the range of
    140      * the track.
    141      *
    142      * @param thumbOffset The offset amount in pixels.
    143      */
    144     public void setThumbOffset(int thumbOffset) {
    145         mThumbOffset = thumbOffset;
    146         invalidate();
    147     }
    148 
    149     /**
    150      * Sets the amount of progress changed via the arrow keys.
    151      *
    152      * @param increment The amount to increment or decrement when the user
    153      *            presses the arrow keys.
    154      */
    155     public void setKeyProgressIncrement(int increment) {
    156         mKeyProgressIncrement = increment < 0 ? -increment : increment;
    157     }
    158 
    159     /**
    160      * Returns the amount of progress changed via the arrow keys.
    161      * <p>
    162      * By default, this will be a value that is derived from the max progress.
    163      *
    164      * @return The amount to increment or decrement when the user presses the
    165      *         arrow keys. This will be positive.
    166      */
    167     public int getKeyProgressIncrement() {
    168         return mKeyProgressIncrement;
    169     }
    170 
    171     @Override
    172     public synchronized void setMax(int max) {
    173         super.setMax(max);
    174 
    175         if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) {
    176             // It will take the user too long to change this via keys, change it
    177             // to something more reasonable
    178             setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20)));
    179         }
    180     }
    181 
    182     @Override
    183     protected boolean verifyDrawable(Drawable who) {
    184         return who == mThumb || super.verifyDrawable(who);
    185     }
    186 
    187     @Override
    188     public void jumpDrawablesToCurrentState() {
    189         super.jumpDrawablesToCurrentState();
    190         if (mThumb != null) mThumb.jumpToCurrentState();
    191     }
    192 
    193     @Override
    194     protected void drawableStateChanged() {
    195         super.drawableStateChanged();
    196 
    197         Drawable progressDrawable = getProgressDrawable();
    198         if (progressDrawable != null) {
    199             progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
    200         }
    201 
    202         if (mThumb != null && mThumb.isStateful()) {
    203             int[] state = getDrawableState();
    204             mThumb.setState(state);
    205         }
    206     }
    207 
    208     @Override
    209     void onProgressRefresh(float scale, boolean fromUser) {
    210         super.onProgressRefresh(scale, fromUser);
    211         Drawable thumb = mThumb;
    212         if (thumb != null) {
    213             setThumbPos(getWidth(), getHeight(), thumb, scale, Integer.MIN_VALUE);
    214             /*
    215              * Since we draw translated, the drawable's bounds that it signals
    216              * for invalidation won't be the actual bounds we want invalidated,
    217              * so just invalidate this whole view.
    218              */
    219             invalidate();
    220         }
    221     }
    222 
    223 
    224     @Override
    225     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    226         updateThumbPos(w, h);
    227     }
    228 
    229     private void updateThumbPos(int w, int h) {
    230         Drawable d = getCurrentDrawable();
    231         Drawable thumb = mThumb;
    232         if (mIsVertical) {
    233             int thumbWidth = thumb == null ? 0 : thumb.getIntrinsicWidth();
    234             // The max width does not incorporate padding, whereas the width
    235             // parameter does
    236             int trackWidth = Math.min(mMaxWidth, w - mPaddingLeft - mPaddingRight);
    237 
    238             int max = getMax();
    239             float scale = max > 0 ? (float) getProgress() / (float) max : 0;
    240 
    241             if (thumbWidth > trackWidth) {
    242                 if (thumb != null) {
    243                     setThumbPos(w, h, thumb, scale, 0);
    244                 }
    245                 int gapForCenteringTrack = (thumbWidth - trackWidth) / 2;
    246                 if (d != null) {
    247                     // Canvas will be translated by the padding, so 0,0 is where we start drawing
    248                     d.setBounds(gapForCenteringTrack, 0,
    249                             w - mPaddingRight - gapForCenteringTrack - mPaddingLeft,
    250                             h - mPaddingBottom - mPaddingTop);
    251                 }
    252             } else {
    253                 if (d != null) {
    254                     // Canvas will be translated by the padding, so 0,0 is where we start drawing
    255                     d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
    256                             - mPaddingTop);
    257                 }
    258                 int gap = (trackWidth - thumbWidth) / 2;
    259                 if (thumb != null) {
    260                     setThumbPos(w, h, thumb, scale, gap);
    261                 }
    262             }
    263         } else {
    264             int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
    265             // The max height does not incorporate padding, whereas the height
    266             // parameter does
    267             int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
    268 
    269             int max = getMax();
    270             float scale = max > 0 ? (float) getProgress() / (float) max : 0;
    271 
    272             if (thumbHeight > trackHeight) {
    273                 if (thumb != null) {
    274                     setThumbPos(w, h, thumb, scale, 0);
    275                 }
    276                 int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
    277                 if (d != null) {
    278                     // Canvas will be translated by the padding, so 0,0 is where we start drawing
    279                     d.setBounds(0, gapForCenteringTrack,
    280                             w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
    281                             - mPaddingTop);
    282                 }
    283             } else {
    284                 if (d != null) {
    285                     // Canvas will be translated by the padding, so 0,0 is where we start drawing
    286                     d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
    287                             - mPaddingTop);
    288                 }
    289                 int gap = (trackHeight - thumbHeight) / 2;
    290                 if (thumb != null) {
    291                     setThumbPos(w, h, thumb, scale, gap);
    292                 }
    293             }
    294         }
    295     }
    296 
    297     /**
    298      * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
    299      */
    300     private void setThumbPos(int w, int h, Drawable thumb, float scale, int gap) {
    301         int available;
    302         int thumbWidth = thumb.getIntrinsicWidth();
    303         int thumbHeight = thumb.getIntrinsicHeight();
    304         if (mIsVertical) {
    305             available = h - mPaddingTop - mPaddingBottom - thumbHeight;
    306         } else {
    307             available = w - mPaddingLeft - mPaddingRight - thumbWidth;
    308         }
    309 
    310         // The extra space for the thumb to move on the track
    311         available += mThumbOffset * 2;
    312 
    313 
    314         if (mIsVertical) {
    315             int thumbPos = (int) ((1.0f - scale) * available);
    316             int leftBound, rightBound;
    317             if (gap == Integer.MIN_VALUE) {
    318                 Rect oldBounds = thumb.getBounds();
    319                 leftBound = oldBounds.left;
    320                 rightBound = oldBounds.right;
    321             } else {
    322                 leftBound = gap;
    323                 rightBound = gap + thumbWidth;
    324             }
    325 
    326             // Canvas will be translated, so 0,0 is where we start drawing
    327             thumb.setBounds(leftBound, thumbPos, rightBound, thumbPos + thumbHeight);
    328         } else {
    329             int thumbPos = (int) (scale * available);
    330             int topBound, bottomBound;
    331             if (gap == Integer.MIN_VALUE) {
    332                 Rect oldBounds = thumb.getBounds();
    333                 topBound = oldBounds.top;
    334                 bottomBound = oldBounds.bottom;
    335             } else {
    336                 topBound = gap;
    337                 bottomBound = gap + thumbHeight;
    338             }
    339 
    340             // Canvas will be translated, so 0,0 is where we start drawing
    341             thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound);
    342         }
    343     }
    344 
    345     @Override
    346     protected synchronized void onDraw(Canvas canvas) {
    347         super.onDraw(canvas);
    348         if (mThumb != null) {
    349             canvas.save();
    350             // Translate the padding. For the x/y, we need to allow the thumb to
    351             // draw in its extra space
    352             if (mIsVertical) {
    353                 canvas.translate(mPaddingLeft, mPaddingTop - mThumbOffset);
    354                 mThumb.draw(canvas);
    355                 canvas.restore();
    356             } else {
    357                 canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
    358                 mThumb.draw(canvas);
    359                 canvas.restore();
    360             }
    361         }
    362     }
    363 
    364     @Override
    365     protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    366         Drawable d = getCurrentDrawable();
    367 
    368         int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
    369         int dw = 0;
    370         int dh = 0;
    371         if (d != null) {
    372             dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
    373             dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
    374             dh = Math.max(thumbHeight, dh);
    375         }
    376         dw += mPaddingLeft + mPaddingRight;
    377         dh += mPaddingTop + mPaddingBottom;
    378 
    379         setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
    380                 resolveSizeAndState(dh, heightMeasureSpec, 0));
    381 
    382         // TODO should probably make this an explicit attribute instead of implicitly
    383         // setting it based on the size
    384         if (getMeasuredHeight() > getMeasuredWidth()) {
    385             mIsVertical = true;
    386         }
    387     }
    388 
    389     @Override
    390     public boolean onTouchEvent(MotionEvent event) {
    391         if (!mIsUserSeekable || !isEnabled()) {
    392             return false;
    393         }
    394 
    395         switch (event.getAction()) {
    396             case MotionEvent.ACTION_DOWN:
    397                 setPressed(true);
    398                 onStartTrackingTouch();
    399                 trackTouchEvent(event);
    400                 break;
    401 
    402             case MotionEvent.ACTION_MOVE:
    403                 trackTouchEvent(event);
    404                 attemptClaimDrag();
    405                 break;
    406 
    407             case MotionEvent.ACTION_UP:
    408                 trackTouchEvent(event);
    409                 onStopTrackingTouch();
    410                 setPressed(false);
    411                 // ProgressBar doesn't know to repaint the thumb drawable
    412                 // in its inactive state when the touch stops (because the
    413                 // value has not apparently changed)
    414                 invalidate();
    415                 break;
    416 
    417             case MotionEvent.ACTION_CANCEL:
    418                 onStopTrackingTouch();
    419                 setPressed(false);
    420                 invalidate(); // see above explanation
    421                 break;
    422         }
    423         return true;
    424     }
    425 
    426     private void trackTouchEvent(MotionEvent event) {
    427         float progress = 0;
    428         if (mIsVertical) {
    429             final int height = getHeight();
    430             final int available = height - mPaddingTop - mPaddingBottom;
    431             int y = (int)event.getY();
    432             float scale;
    433             if (y < mPaddingTop) {
    434                 scale = 1.0f;
    435             } else if (y > height - mPaddingBottom) {
    436                 scale = 0.0f;
    437             } else {
    438                 scale = 1.0f - (float)(y - mPaddingTop) / (float)available;
    439                 progress = mTouchProgressOffset;
    440             }
    441 
    442             final int max = getMax();
    443             progress += scale * max;
    444         } else {
    445             final int width = getWidth();
    446             final int available = width - mPaddingLeft - mPaddingRight;
    447             int x = (int)event.getX();
    448             float scale;
    449             if (x < mPaddingLeft) {
    450                 scale = 0.0f;
    451             } else if (x > width - mPaddingRight) {
    452                 scale = 1.0f;
    453             } else {
    454                 scale = (float)(x - mPaddingLeft) / (float)available;
    455                 progress = mTouchProgressOffset;
    456             }
    457 
    458             final int max = getMax();
    459             progress += scale * max;
    460         }
    461 
    462         setProgress((int) progress, true);
    463     }
    464 
    465     /**
    466      * Tries to claim the user's drag motion, and requests disallowing any
    467      * ancestors from stealing events in the drag.
    468      */
    469     private void attemptClaimDrag() {
    470         if (mParent != null) {
    471             mParent.requestDisallowInterceptTouchEvent(true);
    472         }
    473     }
    474 
    475     /**
    476      * This is called when the user has started touching this widget.
    477      */
    478     void onStartTrackingTouch() {
    479     }
    480 
    481     /**
    482      * This is called when the user either releases his touch or the touch is
    483      * canceled.
    484      */
    485     void onStopTrackingTouch() {
    486     }
    487 
    488     /**
    489      * Called when the user changes the seekbar's progress by using a key event.
    490      */
    491     void onKeyChange() {
    492     }
    493 
    494     @Override
    495     public boolean onKeyDown(int keyCode, KeyEvent event) {
    496         if (isEnabled()) {
    497             int progress = getProgress();
    498             if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !mIsVertical)
    499                     || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && mIsVertical)) {
    500                 if (progress > 0) {
    501                     setProgress(progress - mKeyProgressIncrement, true);
    502                     onKeyChange();
    503                     return true;
    504                 }
    505             } else if ((keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && !mIsVertical)
    506                     || (keyCode == KeyEvent.KEYCODE_DPAD_UP && mIsVertical)) {
    507                 if (progress < getMax()) {
    508                     setProgress(progress + mKeyProgressIncrement, true);
    509                     onKeyChange();
    510                     return true;
    511                 }
    512             }
    513         }
    514 
    515         return super.onKeyDown(keyCode, event);
    516     }
    517 
    518 }
    519