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