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.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     /**
     44      * On key presses (right or left), the amount to increment/decrement the
     45      * progress.
     46      */
     47     private int mKeyProgressIncrement = 1;
     48 
     49     private static final int NO_ALPHA = 0xFF;
     50     private float mDisabledAlpha;
     51 
     52     public AbsSeekBar(Context context) {
     53         super(context);
     54     }
     55 
     56     public AbsSeekBar(Context context, AttributeSet attrs) {
     57         super(context, attrs);
     58     }
     59 
     60     public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) {
     61         super(context, attrs, defStyle);
     62 
     63         TypedArray a = context.obtainStyledAttributes(attrs,
     64                 com.android.internal.R.styleable.SeekBar, defStyle, 0);
     65         Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb);
     66         setThumb(thumb); // will guess mThumbOffset if thumb != null...
     67         // ...but allow layout to override this
     68         int thumbOffset = a.getDimensionPixelOffset(
     69                 com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset());
     70         setThumbOffset(thumbOffset);
     71         a.recycle();
     72 
     73         a = context.obtainStyledAttributes(attrs,
     74                 com.android.internal.R.styleable.Theme, 0, 0);
     75         mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);
     76         a.recycle();
     77     }
     78 
     79     /**
     80      * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
     81      * <p>
     82      * If the thumb is a valid drawable (i.e. not null), half its width will be
     83      * used as the new thumb offset (@see #setThumbOffset(int)).
     84      *
     85      * @param thumb Drawable representing the thumb
     86      */
     87     public void setThumb(Drawable thumb) {
     88         if (thumb != null) {
     89             thumb.setCallback(this);
     90 
     91             // Assuming the thumb drawable is symmetric, set the thumb offset
     92             // such that the thumb will hang halfway off either edge of the
     93             // progress bar.
     94             mThumbOffset = thumb.getIntrinsicWidth() / 2;
     95         }
     96         mThumb = thumb;
     97         invalidate();
     98     }
     99 
    100     /**
    101      * @see #setThumbOffset(int)
    102      */
    103     public int getThumbOffset() {
    104         return mThumbOffset;
    105     }
    106 
    107     /**
    108      * Sets the thumb offset that allows the thumb to extend out of the range of
    109      * the track.
    110      *
    111      * @param thumbOffset The offset amount in pixels.
    112      */
    113     public void setThumbOffset(int thumbOffset) {
    114         mThumbOffset = thumbOffset;
    115         invalidate();
    116     }
    117 
    118     /**
    119      * Sets the amount of progress changed via the arrow keys.
    120      *
    121      * @param increment The amount to increment or decrement when the user
    122      *            presses the arrow keys.
    123      */
    124     public void setKeyProgressIncrement(int increment) {
    125         mKeyProgressIncrement = increment < 0 ? -increment : increment;
    126     }
    127 
    128     /**
    129      * Returns the amount of progress changed via the arrow keys.
    130      * <p>
    131      * By default, this will be a value that is derived from the max progress.
    132      *
    133      * @return The amount to increment or decrement when the user presses the
    134      *         arrow keys. This will be positive.
    135      */
    136     public int getKeyProgressIncrement() {
    137         return mKeyProgressIncrement;
    138     }
    139 
    140     @Override
    141     public synchronized void setMax(int max) {
    142         super.setMax(max);
    143 
    144         if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) {
    145             // It will take the user too long to change this via keys, change it
    146             // to something more reasonable
    147             setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20)));
    148         }
    149     }
    150 
    151     @Override
    152     protected boolean verifyDrawable(Drawable who) {
    153         return who == mThumb || super.verifyDrawable(who);
    154     }
    155 
    156     @Override
    157     protected void drawableStateChanged() {
    158         super.drawableStateChanged();
    159 
    160         Drawable progressDrawable = getProgressDrawable();
    161         if (progressDrawable != null) {
    162             progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
    163         }
    164 
    165         if (mThumb != null && mThumb.isStateful()) {
    166             int[] state = getDrawableState();
    167             mThumb.setState(state);
    168         }
    169     }
    170 
    171     @Override
    172     void onProgressRefresh(float scale, boolean fromUser) {
    173         Drawable thumb = mThumb;
    174         if (thumb != null) {
    175             setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE);
    176             /*
    177              * Since we draw translated, the drawable's bounds that it signals
    178              * for invalidation won't be the actual bounds we want invalidated,
    179              * so just invalidate this whole view.
    180              */
    181             invalidate();
    182         }
    183     }
    184 
    185 
    186     @Override
    187     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    188         Drawable d = getCurrentDrawable();
    189         Drawable thumb = mThumb;
    190         int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
    191         // The max height does not incorporate padding, whereas the height
    192         // parameter does
    193         int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
    194 
    195         int max = getMax();
    196         float scale = max > 0 ? (float) getProgress() / (float) max : 0;
    197 
    198         if (thumbHeight > trackHeight) {
    199             if (thumb != null) {
    200                 setThumbPos(w, thumb, scale, 0);
    201             }
    202             int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
    203             if (d != null) {
    204                 // Canvas will be translated by the padding, so 0,0 is where we start drawing
    205                 d.setBounds(0, gapForCenteringTrack,
    206                         w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
    207                         - mPaddingTop);
    208             }
    209         } else {
    210             if (d != null) {
    211                 // Canvas will be translated by the padding, so 0,0 is where we start drawing
    212                 d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
    213                         - mPaddingTop);
    214             }
    215             int gap = (trackHeight - thumbHeight) / 2;
    216             if (thumb != null) {
    217                 setThumbPos(w, thumb, scale, gap);
    218             }
    219         }
    220     }
    221 
    222     /**
    223      * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
    224      */
    225     private void setThumbPos(int w, Drawable thumb, float scale, int gap) {
    226         int available = w - mPaddingLeft - mPaddingRight;
    227         int thumbWidth = thumb.getIntrinsicWidth();
    228         int thumbHeight = thumb.getIntrinsicHeight();
    229         available -= thumbWidth;
    230 
    231         // The extra space for the thumb to move on the track
    232         available += mThumbOffset * 2;
    233 
    234         int thumbPos = (int) (scale * available);
    235 
    236         int topBound, bottomBound;
    237         if (gap == Integer.MIN_VALUE) {
    238             Rect oldBounds = thumb.getBounds();
    239             topBound = oldBounds.top;
    240             bottomBound = oldBounds.bottom;
    241         } else {
    242             topBound = gap;
    243             bottomBound = gap + thumbHeight;
    244         }
    245 
    246         // Canvas will be translated, so 0,0 is where we start drawing
    247         thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound);
    248     }
    249 
    250     @Override
    251     protected synchronized void onDraw(Canvas canvas) {
    252         super.onDraw(canvas);
    253         if (mThumb != null) {
    254             canvas.save();
    255             // Translate the padding. For the x, we need to allow the thumb to
    256             // draw in its extra space
    257             canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
    258             mThumb.draw(canvas);
    259             canvas.restore();
    260         }
    261     }
    262 
    263     @Override
    264     protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    265         Drawable d = getCurrentDrawable();
    266 
    267         int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
    268         int dw = 0;
    269         int dh = 0;
    270         if (d != null) {
    271             dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
    272             dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
    273             dh = Math.max(thumbHeight, dh);
    274         }
    275         dw += mPaddingLeft + mPaddingRight;
    276         dh += mPaddingTop + mPaddingBottom;
    277 
    278         setMeasuredDimension(resolveSize(dw, widthMeasureSpec),
    279                 resolveSize(dh, heightMeasureSpec));
    280     }
    281 
    282     @Override
    283     public boolean onTouchEvent(MotionEvent event) {
    284         if (!mIsUserSeekable || !isEnabled()) {
    285             return false;
    286         }
    287 
    288         switch (event.getAction()) {
    289             case MotionEvent.ACTION_DOWN:
    290                 setPressed(true);
    291                 onStartTrackingTouch();
    292                 trackTouchEvent(event);
    293                 break;
    294 
    295             case MotionEvent.ACTION_MOVE:
    296                 trackTouchEvent(event);
    297                 attemptClaimDrag();
    298                 break;
    299 
    300             case MotionEvent.ACTION_UP:
    301                 trackTouchEvent(event);
    302                 onStopTrackingTouch();
    303                 setPressed(false);
    304                 // ProgressBar doesn't know to repaint the thumb drawable
    305                 // in its inactive state when the touch stops (because the
    306                 // value has not apparently changed)
    307                 invalidate();
    308                 break;
    309 
    310             case MotionEvent.ACTION_CANCEL:
    311                 onStopTrackingTouch();
    312                 setPressed(false);
    313                 invalidate(); // see above explanation
    314                 break;
    315         }
    316         return true;
    317     }
    318 
    319     private void trackTouchEvent(MotionEvent event) {
    320         final int width = getWidth();
    321         final int available = width - mPaddingLeft - mPaddingRight;
    322         int x = (int)event.getX();
    323         float scale;
    324         float progress = 0;
    325         if (x < mPaddingLeft) {
    326             scale = 0.0f;
    327         } else if (x > width - mPaddingRight) {
    328             scale = 1.0f;
    329         } else {
    330             scale = (float)(x - mPaddingLeft) / (float)available;
    331             progress = mTouchProgressOffset;
    332         }
    333 
    334         final int max = getMax();
    335         progress += scale * max;
    336 
    337         setProgress((int) progress, true);
    338     }
    339 
    340     /**
    341      * Tries to claim the user's drag motion, and requests disallowing any
    342      * ancestors from stealing events in the drag.
    343      */
    344     private void attemptClaimDrag() {
    345         if (mParent != null) {
    346             mParent.requestDisallowInterceptTouchEvent(true);
    347         }
    348     }
    349 
    350     /**
    351      * This is called when the user has started touching this widget.
    352      */
    353     void onStartTrackingTouch() {
    354     }
    355 
    356     /**
    357      * This is called when the user either releases his touch or the touch is
    358      * canceled.
    359      */
    360     void onStopTrackingTouch() {
    361     }
    362 
    363     /**
    364      * Called when the user changes the seekbar's progress by using a key event.
    365      */
    366     void onKeyChange() {
    367     }
    368 
    369     @Override
    370     public boolean onKeyDown(int keyCode, KeyEvent event) {
    371         if (isEnabled()) {
    372             int progress = getProgress();
    373             switch (keyCode) {
    374                 case KeyEvent.KEYCODE_DPAD_LEFT:
    375                     if (progress <= 0) break;
    376                     setProgress(progress - mKeyProgressIncrement, true);
    377                     onKeyChange();
    378                     return true;
    379 
    380                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    381                     if (progress >= getMax()) break;
    382                     setProgress(progress + mKeyProgressIncrement, true);
    383                     onKeyChange();
    384                     return true;
    385             }
    386         }
    387 
    388         return super.onKeyDown(keyCode, event);
    389     }
    390 
    391 }
    392