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