NumberPicker.java revision 50f34d14f6dd3411fdbdb6a7b8b285c2b8fdbf5c
1/*
2 * Copyright (C) 2008 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
17package android.widget;
18
19import com.android.internal.R;
20
21import android.animation.Animator;
22import android.animation.AnimatorListenerAdapter;
23import android.animation.AnimatorSet;
24import android.animation.ObjectAnimator;
25import android.animation.ValueAnimator;
26import android.annotation.Widget;
27import android.content.Context;
28import android.content.res.ColorStateList;
29import android.content.res.TypedArray;
30import android.graphics.Canvas;
31import android.graphics.Color;
32import android.graphics.Paint;
33import android.graphics.Paint.Align;
34import android.graphics.Rect;
35import android.text.InputFilter;
36import android.text.InputType;
37import android.text.Spanned;
38import android.text.TextUtils;
39import android.text.method.NumberKeyListener;
40import android.util.AttributeSet;
41import android.util.SparseArray;
42import android.view.KeyEvent;
43import android.view.LayoutInflater;
44import android.view.LayoutInflater.Filter;
45import android.view.MotionEvent;
46import android.view.VelocityTracker;
47import android.view.View;
48import android.view.ViewConfiguration;
49import android.view.animation.OvershootInterpolator;
50import android.view.inputmethod.InputMethodManager;
51
52/**
53 * A view for selecting a number For a dialog using this view, see
54 * {@link android.app.TimePickerDialog}.
55 *
56 * @hide
57 */
58@Widget
59public class NumberPicker extends LinearLayout {
60
61    /**
62     * The index of the middle selector item.
63     */
64    private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2;
65
66    /**
67     * The coefficient by which to adjust (divide) the max fling velocity.
68     */
69    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
70
71    /**
72     * The the duration for adjusting the selector wheel.
73     */
74    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
75
76    /**
77     * The the delay for showing the input controls after a single tap on the
78     * input text.
79     */
80    private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration
81            .getDoubleTapTimeout();
82
83    /**
84     * The update step for incrementing the current value.
85     */
86    private static final int UPDATE_STEP_INCREMENT = 1;
87
88    /**
89     * The update step for decrementing the current value.
90     */
91    private static final int UPDATE_STEP_DECREMENT = -1;
92
93    /**
94     * The strength of fading in the top and bottom while drawing the selector.
95     */
96    private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
97
98    /**
99     * The numbers accepted by the input text's {@link Filter}
100     */
101    private static final char[] DIGIT_CHARACTERS = new char[] {
102            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
103    };
104
105    /**
106     * Use a custom NumberPicker formatting callback to use two-digit minutes
107     * strings like "01". Keeping a static formatter etc. is the most efficient
108     * way to do this; it avoids creating temporary objects on every call to
109     * format().
110     */
111    public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
112        final StringBuilder mBuilder = new StringBuilder();
113
114        final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
115
116        final Object[] mArgs = new Object[1];
117
118        public String toString(int value) {
119            mArgs[0] = value;
120            mBuilder.delete(0, mBuilder.length());
121            mFmt.format("%02d", mArgs);
122            return mFmt.toString();
123        }
124    };
125
126    /**
127     * The increment button.
128     */
129    private final ImageButton mIncrementButton;
130
131    /**
132     * The decrement button.
133     */
134    private final ImageButton mDecrementButton;
135
136    /**
137     * The text for showing the current value.
138     */
139    private final EditText mInputText;
140
141    /**
142     * The height of the text.
143     */
144    private final int mTextSize;
145
146    /**
147     * The values to be displayed instead the indices.
148     */
149    private String[] mDisplayedValues;
150
151    /**
152     * Lower value of the range of numbers allowed for the NumberPicker
153     */
154    private int mStart;
155
156    /**
157     * Upper value of the range of numbers allowed for the NumberPicker
158     */
159    private int mEnd;
160
161    /**
162     * Current value of this NumberPicker
163     */
164    private int mCurrent;
165
166    /**
167     * Listener to be notified upon current value change.
168     */
169    private OnChangeListener mOnChangeListener;
170
171    /**
172     * Listener to be notified upon scroll state change.
173     */
174    private OnScrollListener mOnScrollListener;
175
176    /**
177     * Formatter for for displaying the current value.
178     */
179    private Formatter mFormatter;
180
181    /**
182     * The speed for updating the value form long press.
183     */
184    private long mLongPressUpdateSpeed = 300;
185
186    /**
187     * Cache for the string representation of selector indices.
188     */
189    private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
190
191    /**
192     * The selector indices whose value are show by the selector.
193     */
194    private final int[] mSelectorIndices = new int[] {
195            Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE,
196            Integer.MIN_VALUE
197    };
198
199    /**
200     * The {@link Paint} for drawing the selector.
201     */
202    private final Paint mSelectorPaint;
203
204    /**
205     * The height of a selector element (text + gap).
206     */
207    private int mSelectorElementHeight;
208
209    /**
210     * The initial offset of the scroll selector.
211     */
212    private int mInitialScrollOffset = Integer.MIN_VALUE;
213
214    /**
215     * The current offset of the scroll selector.
216     */
217    private int mCurrentScrollOffset;
218
219    /**
220     * The {@link Scroller} responsible for flinging the selector.
221     */
222    private final Scroller mFlingScroller;
223
224    /**
225     * The {@link Scroller} responsible for adjusting the selector.
226     */
227    private final Scroller mAdjustScroller;
228
229    /**
230     * The previous Y coordinate while scrolling the selector.
231     */
232    private int mPreviousScrollerY;
233
234    /**
235     * Handle to the reusable command for setting the input text selection.
236     */
237    private SetSelectionCommand mSetSelectionCommand;
238
239    /**
240     * Handle to the reusable command for adjusting the scroller.
241     */
242    private AdjustScrollerCommand mAdjustScrollerCommand;
243
244    /**
245     * Handle to the reusable command for updating the current value from long
246     * press.
247     */
248    private UpdateValueFromLongPressCommand mUpdateFromLongPressCommand;
249
250    /**
251     * {@link Animator} for showing the up/down arrows.
252     */
253    private final AnimatorSet mShowInputControlsAnimator;
254
255    /**
256     * The Y position of the last down event.
257     */
258    private float mLastDownEventY;
259
260    /**
261     * The Y position of the last motion event.
262     */
263    private float mLastMotionEventY;
264
265    /**
266     * Flag if to begin edit on next up event.
267     */
268    private boolean mBeginEditOnUpEvent;
269
270    /**
271     * Flag if to adjust the selector wheel on next up event.
272     */
273    private boolean mAdjustScrollerOnUpEvent;
274
275    /**
276     * Flag if to draw the selector wheel.
277     */
278    private boolean mDrawSelectorWheel;
279
280    /**
281     * Determines speed during touch scrolling.
282     */
283    private VelocityTracker mVelocityTracker;
284
285    /**
286     * @see ViewConfiguration#getScaledTouchSlop()
287     */
288    private int mTouchSlop;
289
290    /**
291     * @see ViewConfiguration#getScaledMinimumFlingVelocity()
292     */
293    private int mMinimumFlingVelocity;
294
295    /**
296     * @see ViewConfiguration#getScaledMaximumFlingVelocity()
297     */
298    private int mMaximumFlingVelocity;
299
300    /**
301     * Flag whether the selector should wrap around.
302     */
303    private boolean mWrapSelector;
304
305    /**
306     * The back ground color used to optimize scroller fading.
307     */
308    private final int mSolidColor;
309
310    /**
311     * Reusable {@link Rect} instance.
312     */
313    private final Rect mTempRect = new Rect();
314
315    /**
316     * The current scroll state of the number picker.
317     */
318    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
319
320    /**
321     * The callback interface used to indicate the number value has changed.
322     */
323    public interface OnChangeListener {
324        /**
325         * @param picker The NumberPicker associated with this listener.
326         * @param oldVal The previous value.
327         * @param newVal The new value.
328         */
329        void onChanged(NumberPicker picker, int oldVal, int newVal);
330    }
331
332    /**
333     * Interface for listening to the picker scroll state.
334     */
335    public interface OnScrollListener {
336
337        /**
338         * The view is not scrolling.
339         */
340        public static int SCROLL_STATE_IDLE = 0;
341
342        /**
343         * The user is scrolling using touch, and their finger is still on the screen.
344         */
345        public static int SCROLL_STATE_TOUCH_SCROLL = 1;
346
347        /**
348         * The user had previously been scrolling using touch and performed a fling.
349         */
350        public static int SCROLL_STATE_FLING = 2;
351
352        /**
353         * Callback method to be invoked while the number picker is being scrolled.
354         *
355         * @param view The view whose scroll state is being reported
356         * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
357         * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
358         */
359        public void onScrollStateChanged(NumberPicker view, int scrollState);
360    }
361
362    /**
363     * Interface used to format the number into a string for presentation
364     */
365    public interface Formatter {
366        String toString(int value);
367    }
368
369    /**
370     * Create a new number picker
371     *
372     * @param context The application environment.
373     * @param attrs A collection of attributes.
374     */
375    public NumberPicker(Context context, AttributeSet attrs) {
376        this(context, attrs, R.attr.numberPickerStyle);
377    }
378
379    /**
380     * Create a new number picker
381     *
382     * @param context the application environment.
383     * @param attrs a collection of attributes.
384     * @param defStyle The default style to apply to this view.
385     */
386    public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
387        super(context, attrs, defStyle);
388
389        // process style attributes
390        TypedArray attributesArray = context.obtainStyledAttributes(attrs,
391                R.styleable.NumberPicker, defStyle, 0);
392        int orientation = attributesArray.getInt(R.styleable.NumberPicker_orientation, VERTICAL);
393        setOrientation(orientation);
394        mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
395        attributesArray.recycle();
396
397        // By default Linearlayout that we extend is not drawn. This is
398        // its draw() method is not called but dispatchDraw() is called
399        // directly (see ViewGroup.drawChild()). However, this class uses
400        // the fading edge effect implemented by View and we need our
401        // draw() method to be called. Therefore, we declare we will draw.
402        setWillNotDraw(false);
403        setDrawSelectorWheel(false);
404
405        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
406                Context.LAYOUT_INFLATER_SERVICE);
407        inflater.inflate(R.layout.number_picker, this, true);
408
409        OnClickListener onClickListener = new OnClickListener() {
410            public void onClick(View v) {
411                mInputText.clearFocus();
412                if (v.getId() == R.id.increment) {
413                    changeCurrent(mCurrent + 1);
414                } else {
415                    changeCurrent(mCurrent - 1);
416                }
417            }
418        };
419
420        OnLongClickListener onLongClickListener = new OnLongClickListener() {
421            public boolean onLongClick(View v) {
422                mInputText.clearFocus();
423                if (v.getId() == R.id.increment) {
424                    postUpdateValueFromLongPress(UPDATE_STEP_INCREMENT);
425                } else {
426                    postUpdateValueFromLongPress(UPDATE_STEP_DECREMENT);
427                }
428                return true;
429            }
430        };
431
432        // increment button
433        mIncrementButton = (ImageButton) findViewById(R.id.increment);
434        mIncrementButton.setOnClickListener(onClickListener);
435        mIncrementButton.setOnLongClickListener(onLongClickListener);
436
437        // decrement button
438        mDecrementButton = (ImageButton) findViewById(R.id.decrement);
439        mDecrementButton.setOnClickListener(onClickListener);
440        mDecrementButton.setOnLongClickListener(onLongClickListener);
441
442        // input text
443        mInputText = (EditText) findViewById(R.id.timepicker_input);
444        mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
445            public void onFocusChange(View v, boolean hasFocus) {
446                if (!hasFocus) {
447                    validateInputTextView(v);
448                }
449            }
450        });
451        mInputText.setFilters(new InputFilter[] {
452            new InputTextFilter()
453        });
454
455        mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
456
457        // initialize constants
458        mTouchSlop = ViewConfiguration.getTapTimeout();
459        ViewConfiguration configuration = ViewConfiguration.get(context);
460        mTouchSlop = configuration.getScaledTouchSlop();
461        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
462        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
463                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
464        mTextSize = (int) mInputText.getTextSize();
465
466        // create the selector wheel paint
467        Paint paint = new Paint();
468        paint.setAntiAlias(true);
469        paint.setTextAlign(Align.CENTER);
470        paint.setTextSize(mTextSize);
471        paint.setTypeface(mInputText.getTypeface());
472        ColorStateList colors = mInputText.getTextColors();
473        int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
474        paint.setColor(color);
475        mSelectorPaint = paint;
476
477        // create the animator for showing the input controls
478        final ValueAnimator fadeScroller = ObjectAnimator.ofInt(this, "selectorPaintAlpha", 255, 0);
479        final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton,
480                "alpha", 0, 1);
481        final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton,
482                "alpha", 0, 1);
483        mShowInputControlsAnimator = new AnimatorSet();
484        mShowInputControlsAnimator.playTogether(fadeScroller, showIncrementButton,
485                showDecrementButton);
486        mShowInputControlsAnimator.setDuration(getResources().getInteger(
487                R.integer.config_longAnimTime));
488        mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() {
489            private boolean mCanceled = false;
490
491            @Override
492            public void onAnimationEnd(Animator animation) {
493                if (!mCanceled) {
494                    // if canceled => we still want the wheel drawn
495                    setDrawSelectorWheel(false);
496                }
497                mCanceled = false;
498                mSelectorPaint.setAlpha(255);
499                invalidate();
500            }
501
502            @Override
503            public void onAnimationCancel(Animator animation) {
504                if (mShowInputControlsAnimator.isRunning()) {
505                    mCanceled = true;
506                }
507            }
508        });
509
510        // create the fling and adjust scrollers
511        mFlingScroller = new Scroller(getContext());
512        mAdjustScroller = new Scroller(getContext(), new OvershootInterpolator());
513
514        updateInputTextView();
515        updateIncrementAndDecrementButtonsVisibilityState();
516    }
517
518    @Override
519    public void onWindowFocusChanged(boolean hasWindowFocus) {
520        super.onWindowFocusChanged(hasWindowFocus);
521        if (!hasWindowFocus) {
522            removeAllCallbacks();
523        }
524    }
525
526    @Override
527    public boolean onInterceptTouchEvent(MotionEvent event) {
528        switch (event.getActionMasked()) {
529            case MotionEvent.ACTION_DOWN:
530                mLastMotionEventY = mLastDownEventY = event.getY();
531                removeAllCallbacks();
532                mBeginEditOnUpEvent = false;
533                mAdjustScrollerOnUpEvent = true;
534                if (mDrawSelectorWheel) {
535                    boolean scrollersFinished = mFlingScroller.isFinished()
536                            && mAdjustScroller.isFinished();
537                    if (!scrollersFinished) {
538                        mFlingScroller.forceFinished(true);
539                        mAdjustScroller.forceFinished(true);
540                        tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE);
541                    }
542                    mBeginEditOnUpEvent = scrollersFinished;
543                    mAdjustScrollerOnUpEvent = true;
544                    hideInputControls();
545                    return true;
546                }
547                if (isEventInInputText(event)) {
548                    mAdjustScrollerOnUpEvent = false;
549                    setDrawSelectorWheel(true);
550                    hideInputControls();
551                    return true;
552                }
553                break;
554            case MotionEvent.ACTION_MOVE:
555                float currentMoveY = event.getY();
556                int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
557                if (deltaDownY > mTouchSlop) {
558                    mBeginEditOnUpEvent = false;
559                    tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
560                    setDrawSelectorWheel(true);
561                    hideInputControls();
562                    return true;
563                }
564                break;
565        }
566        return false;
567    }
568
569    @Override
570    public boolean onTouchEvent(MotionEvent ev) {
571        if (mVelocityTracker == null) {
572            mVelocityTracker = VelocityTracker.obtain();
573        }
574        mVelocityTracker.addMovement(ev);
575        int action = ev.getActionMasked();
576        switch (action) {
577            case MotionEvent.ACTION_MOVE:
578                float currentMoveY = ev.getY();
579                if (mBeginEditOnUpEvent
580                        || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
581                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
582                    if (deltaDownY > mTouchSlop) {
583                        mBeginEditOnUpEvent = false;
584                        tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
585                    }
586                }
587                int deltaMoveY = (int) (currentMoveY - mLastMotionEventY);
588                scrollBy(0, deltaMoveY);
589                invalidate();
590                mLastMotionEventY = currentMoveY;
591                break;
592            case MotionEvent.ACTION_UP:
593                if (mBeginEditOnUpEvent) {
594                    setDrawSelectorWheel(false);
595                    showInputControls();
596                    mInputText.requestFocus();
597                    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
598                            Context.INPUT_METHOD_SERVICE);
599                    imm.showSoftInput(mInputText, 0);
600                    mInputText.setSelection(0, mInputText.getText().length());
601                    return true;
602                }
603                VelocityTracker velocityTracker = mVelocityTracker;
604                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
605                int initialVelocity = (int) velocityTracker.getYVelocity();
606                if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
607                    fling(initialVelocity);
608                    tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_FLING);
609                } else {
610                    if (mAdjustScrollerOnUpEvent) {
611                        if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) {
612                            postAdjustScrollerCommand(0);
613                        }
614                    } else {
615                        postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS);
616                    }
617                }
618                mVelocityTracker.recycle();
619                mVelocityTracker = null;
620                break;
621        }
622        return true;
623    }
624
625    @Override
626    public boolean dispatchTouchEvent(MotionEvent event) {
627        int action = event.getActionMasked();
628        if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)
629                && !isEventInInputText(event)) {
630            removeAllCallbacks();
631        }
632        return super.dispatchTouchEvent(event);
633    }
634
635    @Override
636    public boolean dispatchKeyEvent(KeyEvent event) {
637        int keyCode = event.getKeyCode();
638        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
639            removeAllCallbacks();
640        }
641        return super.dispatchKeyEvent(event);
642    }
643
644    @Override
645    public boolean dispatchTrackballEvent(MotionEvent event) {
646        int action = event.getActionMasked();
647        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
648            removeAllCallbacks();
649        }
650        return super.dispatchTrackballEvent(event);
651    }
652
653    @Override
654    public void computeScroll() {
655        if (!mDrawSelectorWheel) {
656            return;
657        }
658        Scroller scroller = mFlingScroller;
659        if (scroller.isFinished()) {
660            scroller = mAdjustScroller;
661            if (scroller.isFinished()) {
662                return;
663            }
664        }
665        scroller.computeScrollOffset();
666        int currentScrollerY = scroller.getCurrY();
667        if (mPreviousScrollerY == 0) {
668            mPreviousScrollerY = scroller.getStartY();
669        }
670        scrollBy(0, currentScrollerY - mPreviousScrollerY);
671        mPreviousScrollerY = currentScrollerY;
672        if (scroller.isFinished()) {
673            onScrollerFinished(scroller);
674        } else {
675            invalidate();
676        }
677    }
678
679    /**
680     * Set the enabled state of this view. The interpretation of the enabled
681     * state varies by subclass.
682     *
683     * @param enabled True if this view is enabled, false otherwise.
684     */
685    @Override
686    public void setEnabled(boolean enabled) {
687        super.setEnabled(enabled);
688        mIncrementButton.setEnabled(enabled);
689        mDecrementButton.setEnabled(enabled);
690        mInputText.setEnabled(enabled);
691    }
692
693    /**
694     * Scrolls the selector with the given <code>vertical offset</code>.
695     */
696    @Override
697    public void scrollBy(int x, int y) {
698        int[] selectorIndices = getSelectorIndices();
699        if (mInitialScrollOffset == Integer.MIN_VALUE) {
700            int totalTextHeight = selectorIndices.length * mTextSize;
701            int totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
702            int textGapCount = selectorIndices.length - 1;
703            int selectorTextGapHeight = totalTextGapHeight / textGapCount;
704            // compensate for integer division loss of the components used to
705            // calculate the text gap
706            int integerDivisionLoss = (mTextSize + mBottom - mTop) % textGapCount;
707            mInitialScrollOffset = mCurrentScrollOffset = mTextSize - integerDivisionLoss / 2;
708            mSelectorElementHeight = mTextSize + selectorTextGapHeight;
709        }
710
711        if (!mWrapSelector && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) {
712            mCurrentScrollOffset = mInitialScrollOffset;
713            return;
714        }
715        if (!mWrapSelector && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) {
716            mCurrentScrollOffset = mInitialScrollOffset;
717            return;
718        }
719        mCurrentScrollOffset += y;
720        while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight) {
721            mCurrentScrollOffset -= mSelectorElementHeight;
722            decrementSelectorIndices(selectorIndices);
723            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
724            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) {
725                mCurrentScrollOffset = mInitialScrollOffset;
726            }
727        }
728        while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight) {
729            mCurrentScrollOffset += mSelectorElementHeight;
730            incrementScrollSelectorIndices(selectorIndices);
731            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
732            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) {
733                mCurrentScrollOffset = mInitialScrollOffset;
734            }
735        }
736    }
737
738    /**
739     * Set the callback that indicates the number has been adjusted by the user.
740     *
741     * @param onChangeListener the callback, should not be null.
742     */
743    public void setOnChangeListener(OnChangeListener onChangeListener) {
744        mOnChangeListener = onChangeListener;
745    }
746
747    /**
748     * Set the callback that in notified for scroll state changes.
749     *
750     * @param onScrollListener the callback, should not be null.
751     */
752    public void setOnScrollListener(OnScrollListener onScrollListener) {
753        mOnScrollListener = onScrollListener;
754    }
755
756    /**
757     * Set the formatter that will be used to format the number for presentation
758     *
759     * @param formatter the formatter object. If formatter is null,
760     *            String.valueOf() will be used
761     */
762    public void setFormatter(Formatter formatter) {
763        mFormatter = formatter;
764    }
765
766    /**
767     * Set the range of numbers allowed for the number picker. The current value
768     * will be automatically set to the start.
769     *
770     * @param start the start of the range (inclusive)
771     * @param end the end of the range (inclusive)
772     */
773    public void setRange(int start, int end) {
774        setRange(start, end, null);
775    }
776
777    /**
778     * Set the range of numbers allowed for the number picker. The current value
779     * will be automatically set to the start. Also provide a mapping for values
780     * used to display to the user.
781     *
782     * @param start the start of the range (inclusive)
783     * @param end the end of the range (inclusive)
784     * @param displayedValues the values displayed to the user.
785     */
786    public void setRange(int start, int end, String[] displayedValues) {
787        boolean wrapSelector = (end - start) >= mSelectorIndices.length;
788        setRange(start, end, displayedValues, wrapSelector);
789    }
790
791    /**
792     * Set the range of numbers allowed for the number picker. The current value
793     * will be automatically set to the start. Also provide a mapping for values
794     * used to display to the user.
795     *
796     * @param start the start of the range (inclusive)
797     * @param end the end of the range (inclusive)
798     * @param displayedValues the values displayed to the user.
799     */
800    public void setRange(int start, int end, String[] displayedValues, boolean wrapSelector) {
801        if (start < 0 || end < 0) {
802            throw new IllegalArgumentException("start and end must be > 0");
803        }
804
805        mDisplayedValues = displayedValues;
806        mStart = start;
807        mEnd = end;
808        mCurrent = start;
809
810        setWrapSelector(wrapSelector);
811        updateInputTextView();
812
813        if (displayedValues != null) {
814            // Allow text entry rather than strictly numeric entry.
815            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
816                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
817        } else {
818            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
819        }
820
821        // make sure cached string representations are dropped
822        mSelectorIndexToStringCache.clear();
823    }
824
825    /**
826     * Set the current value for the number picker.
827     *
828     * @param current the current value the start of the range (inclusive)
829     * @throws IllegalArgumentException when current is not within the range of
830     *             of the number picker
831     */
832    public void setCurrent(int current) {
833        if (current < mStart || current > mEnd) {
834            throw new IllegalArgumentException("current should be >= start and <= end");
835        }
836        mCurrent = current;
837        updateInputTextView();
838        updateIncrementAndDecrementButtonsVisibilityState();
839    }
840
841    /**
842     * Sets whether the selector shown during flinging/scrolling should wrap
843     * around the beginning and end values.
844     *
845     * @param wrapSelector Whether to wrap.
846     */
847    public void setWrapSelector(boolean wrapSelector) {
848        if (wrapSelector && (mEnd - mStart) < mSelectorIndices.length) {
849            throw new IllegalStateException("Range less than selector items count.");
850        }
851        if (wrapSelector != mWrapSelector) {
852            // force the selector indices array to be reinitialized
853            mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE;
854            mWrapSelector = wrapSelector;
855        }
856    }
857
858    /**
859     * Sets the speed at which the numbers will scroll when the +/- buttons are
860     * longpressed
861     *
862     * @param speed The speed (in milliseconds) at which the numbers will scroll
863     *            default 300ms
864     */
865    public void setSpeed(long speed) {
866        mLongPressUpdateSpeed = speed;
867    }
868
869    /**
870     * Returns the current value of the NumberPicker
871     *
872     * @return the current value.
873     */
874    public int getCurrent() {
875        return mCurrent;
876    }
877
878    @Override
879    public int getSolidColor() {
880        return mSolidColor;
881    }
882
883    @Override
884    protected float getTopFadingEdgeStrength() {
885        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
886    }
887
888    @Override
889    protected float getBottomFadingEdgeStrength() {
890        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
891    }
892
893    @Override
894    protected void onDetachedFromWindow() {
895        removeAllCallbacks();
896    }
897
898    @Override
899    protected void dispatchDraw(Canvas canvas) {
900        // There is a good reason for doing this. See comments in draw().
901    }
902
903    @Override
904    public void draw(Canvas canvas) {
905        // Dispatch draw to our children only if we are not currently running
906        // the animation for simultaneously fading out the scroll wheel and
907        // showing in the buttons. This class takes advantage of the View
908        // implementation of fading edges effect to draw the selector wheel.
909        // However, in View.draw(), the fading is applied after all the children
910        // have been drawn and we do not want this fading to be applied to the
911        // buttons which are currently showing in. Therefore, we draw our
912        // children
913        // after we have completed drawing ourselves.
914
915        // Draw the selector wheel if needed
916        if (mDrawSelectorWheel) {
917            super.draw(canvas);
918        }
919
920        // Draw our children if we are not showing the selector wheel of fading
921        // it out
922        if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) {
923            long drawTime = getDrawingTime();
924            for (int i = 0, count = getChildCount(); i < count; i++) {
925                View child = getChildAt(i);
926                if (!child.isShown()) {
927                    continue;
928                }
929                drawChild(canvas, getChildAt(i), drawTime);
930            }
931        }
932    }
933
934    @Override
935    protected void onDraw(Canvas canvas) {
936        // we only draw the selector wheel
937        if (!mDrawSelectorWheel) {
938            return;
939        }
940        float x = (mRight - mLeft) / 2;
941        float y = mCurrentScrollOffset;
942
943        int[] selectorIndices = getSelectorIndices();
944        for (int i = 0; i < selectorIndices.length; i++) {
945            int selectorIndex = selectorIndices[i];
946            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
947            canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint);
948            y += mSelectorElementHeight;
949        }
950    }
951
952    /**
953     * Returns the upper value of the range of the NumberPicker
954     *
955     * @return the uppper number of the range.
956     */
957    protected int getEndRange() {
958        return mEnd;
959    }
960
961    /**
962     * Returns the lower value of the range of the NumberPicker
963     *
964     * @return the lower number of the range.
965     */
966    protected int getBeginRange() {
967        return mStart;
968    }
969
970    /**
971     * Sets the current value of this NumberPicker, and sets mPrevious to the
972     * previous value. If current is greater than mEnd less than mStart, the
973     * value of mCurrent is wrapped around. Subclasses can override this to
974     * change the wrapping behavior
975     *
976     * @param current the new value of the NumberPicker
977     */
978    private void changeCurrent(int current) {
979        if (mCurrent == current) {
980            return;
981        }
982        // Wrap around the values if we go past the start or end
983        if (mWrapSelector) {
984            current = getWrappedSelectorIndex(current);
985        }
986        int previous = mCurrent;
987        setCurrent(current);
988        notifyChange(previous, current);
989    }
990
991    /**
992     * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
993     * wheel.
994     */
995    @SuppressWarnings("unused")
996    // Called by ShowInputControlsAnimator via reflection
997    private void setSelectorPaintAlpha(int alpha) {
998        mSelectorPaint.setAlpha(alpha);
999        if (mDrawSelectorWheel) {
1000            invalidate();
1001        }
1002    }
1003
1004    /**
1005     * @return If the <code>event</code> is in the input text.
1006     */
1007    private boolean isEventInInputText(MotionEvent event) {
1008        mInputText.getHitRect(mTempRect);
1009        return mTempRect.contains((int) event.getX(), (int) event.getY());
1010    }
1011
1012    /**
1013     * Sets if to <code>drawSelectionWheel</code>.
1014     */
1015    private void setDrawSelectorWheel(boolean drawSelectorWheel) {
1016        mDrawSelectorWheel = drawSelectorWheel;
1017        // do not fade if the selector wheel not shown
1018        setVerticalFadingEdgeEnabled(drawSelectorWheel);
1019    }
1020
1021    /**
1022     * Callback invoked upon completion of a given <code>scroller</code>.
1023     */
1024    private void onScrollerFinished(Scroller scroller) {
1025        if (scroller == mFlingScroller) {
1026            postAdjustScrollerCommand(0);
1027            tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE);
1028        } else {
1029            showInputControls();
1030            updateInputTextView();
1031        }
1032    }
1033
1034    /**
1035     * Notifies the scroll listener for the given <code>scrollState</code>
1036     * if the scroll state differs from the current scroll state.
1037     */
1038    private void tryNotifyScrollListener(int scrollState) {
1039        if (mOnScrollListener != null && mScrollState != scrollState) {
1040            mScrollState = scrollState;
1041            mOnScrollListener.onScrollStateChanged(this, scrollState);
1042        }
1043    }
1044
1045    /**
1046     * Flings the selector with the given <code>velocityY</code>.
1047     */
1048    private void fling(int velocityY) {
1049        mPreviousScrollerY = 0;
1050        Scroller flingScroller = mFlingScroller;
1051
1052        if (mWrapSelector) {
1053            if (velocityY > 0) {
1054                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1055            } else {
1056                flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1057            }
1058        } else {
1059            if (velocityY > 0) {
1060                int maxY = mTextSize * (mCurrent - mStart);
1061                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY);
1062            } else {
1063                int startY = mTextSize * (mEnd - mCurrent);
1064                int maxY = startY;
1065                flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY);
1066            }
1067        }
1068
1069        postAdjustScrollerCommand(flingScroller.getDuration());
1070        invalidate();
1071    }
1072
1073    /**
1074     * Hides the input controls which is the up/down arrows and the text field.
1075     */
1076    private void hideInputControls() {
1077        mShowInputControlsAnimator.cancel();
1078        mIncrementButton.setVisibility(INVISIBLE);
1079        mDecrementButton.setVisibility(INVISIBLE);
1080        mInputText.setVisibility(INVISIBLE);
1081    }
1082
1083    /**
1084     * Show the input controls by making them visible and animating the alpha
1085     * property up/down arrows.
1086     */
1087    private void showInputControls() {
1088        updateIncrementAndDecrementButtonsVisibilityState();
1089        mInputText.setVisibility(VISIBLE);
1090        mShowInputControlsAnimator.start();
1091    }
1092
1093    /**
1094     * Updates the visibility state of the increment and decrement buttons.
1095     */
1096    private void updateIncrementAndDecrementButtonsVisibilityState() {
1097        if (mWrapSelector || mCurrent < mEnd) {
1098            mIncrementButton.setVisibility(VISIBLE);
1099        } else {
1100            mIncrementButton.setVisibility(INVISIBLE);
1101        }
1102        if (mWrapSelector || mCurrent > mStart) {
1103            mDecrementButton.setVisibility(VISIBLE);
1104        } else {
1105            mDecrementButton.setVisibility(INVISIBLE);
1106        }
1107    }
1108
1109    /**
1110     * @return The selector indices array with proper values with the current as
1111     *         the middle one.
1112     */
1113    private int[] getSelectorIndices() {
1114        int current = getCurrent();
1115        if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) {
1116            for (int i = 0; i < mSelectorIndices.length; i++) {
1117                int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1118                if (mWrapSelector) {
1119                    selectorIndex = getWrappedSelectorIndex(selectorIndex);
1120                }
1121                mSelectorIndices[i] = selectorIndex;
1122                ensureCachedScrollSelectorValue(mSelectorIndices[i]);
1123            }
1124        }
1125        return mSelectorIndices;
1126    }
1127
1128    /**
1129     * @return The wrapped index <code>selectorIndex</code> value.
1130     *         <p>
1131     *         Note: The absolute value of the argument is never larger than
1132     *         mEnd - mStart.
1133     *         </p>
1134     */
1135    private int getWrappedSelectorIndex(int selectorIndex) {
1136        if (selectorIndex > mEnd) {
1137            return (Math.abs(selectorIndex) - mEnd);
1138        } else if (selectorIndex < mStart) {
1139            return (mEnd - Math.abs(selectorIndex));
1140        }
1141        return selectorIndex;
1142    }
1143
1144    /**
1145     * Increments the <code>selectorIndices</code> whose string representations
1146     * will be displayed in the selector.
1147     */
1148    private void incrementScrollSelectorIndices(int[] selectorIndices) {
1149        for (int i = 0; i < selectorIndices.length - 1; i++) {
1150            selectorIndices[i] = selectorIndices[i + 1];
1151        }
1152        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1153        if (mWrapSelector && nextScrollSelectorIndex > mEnd) {
1154            nextScrollSelectorIndex = mStart;
1155        }
1156        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1157        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1158    }
1159
1160    /**
1161     * Decrements the <code>selectorIndices</code> whose string representations
1162     * will be displayed in the selector.
1163     */
1164    private void decrementSelectorIndices(int[] selectorIndices) {
1165        for (int i = selectorIndices.length - 1; i > 0; i--) {
1166            selectorIndices[i] = selectorIndices[i - 1];
1167        }
1168        int nextScrollSelectorIndex = selectorIndices[1] - 1;
1169        if (mWrapSelector && nextScrollSelectorIndex < mStart) {
1170            nextScrollSelectorIndex = mEnd;
1171        }
1172        selectorIndices[0] = nextScrollSelectorIndex;
1173        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1174    }
1175
1176    /**
1177     * Ensures we have a cached string representation of the given <code>
1178     * selectorIndex</code>
1179     * to avoid multiple instantiations of the same string.
1180     */
1181    private void ensureCachedScrollSelectorValue(int selectorIndex) {
1182        SparseArray<String> cache = mSelectorIndexToStringCache;
1183        String scrollSelectorValue = cache.get(selectorIndex);
1184        if (scrollSelectorValue != null) {
1185            return;
1186        }
1187        if (selectorIndex < mStart || selectorIndex > mEnd) {
1188            scrollSelectorValue = "";
1189        } else {
1190            if (mDisplayedValues != null) {
1191                int displayedValueIndex = selectorIndex - mStart;
1192                scrollSelectorValue = mDisplayedValues[displayedValueIndex];
1193            } else {
1194                scrollSelectorValue = formatNumber(selectorIndex);
1195            }
1196        }
1197        cache.put(selectorIndex, scrollSelectorValue);
1198    }
1199
1200    private String formatNumber(int value) {
1201        return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
1202    }
1203
1204    private void validateInputTextView(View v) {
1205        String str = String.valueOf(((TextView) v).getText());
1206        if (TextUtils.isEmpty(str)) {
1207            // Restore to the old value as we don't allow empty values
1208            updateInputTextView();
1209        } else {
1210            // Check the new value and ensure it's in range
1211            int current = getSelectedPos(str.toString());
1212            changeCurrent(current);
1213        }
1214    }
1215
1216    /**
1217     * Updates the view of this NumberPicker. If displayValues were specified in
1218     * {@link #setRange}, the string corresponding to the index specified by the
1219     * current value will be returned. Otherwise, the formatter specified in
1220     * {@link #setFormatter} will be used to format the number.
1221     */
1222    private void updateInputTextView() {
1223        /*
1224         * If we don't have displayed values then use the current number else
1225         * find the correct value in the displayed values for the current
1226         * number.
1227         */
1228        if (mDisplayedValues == null) {
1229            mInputText.setText(formatNumber(mCurrent));
1230        } else {
1231            mInputText.setText(mDisplayedValues[mCurrent - mStart]);
1232        }
1233        mInputText.setSelection(mInputText.getText().length());
1234    }
1235
1236    /**
1237     * Notifies the listener, if registered, of a change of the value of this
1238     * NumberPicker.
1239     */
1240    private void notifyChange(int previous, int current) {
1241        if (mOnChangeListener != null) {
1242            mOnChangeListener.onChanged(this, previous, mCurrent);
1243        }
1244    }
1245
1246    /**
1247     * Posts a command for updating the current value every <code>updateMillis
1248     * </code>.
1249     */
1250    private void postUpdateValueFromLongPress(int updateMillis) {
1251        mInputText.clearFocus();
1252        removeAllCallbacks();
1253        if (mUpdateFromLongPressCommand == null) {
1254            mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand();
1255        }
1256        mUpdateFromLongPressCommand.setUpdateStep(updateMillis);
1257        post(mUpdateFromLongPressCommand);
1258    }
1259
1260    /**
1261     * Removes all pending callback from the message queue.
1262     */
1263    private void removeAllCallbacks() {
1264        if (mUpdateFromLongPressCommand != null) {
1265            removeCallbacks(mUpdateFromLongPressCommand);
1266        }
1267        if (mAdjustScrollerCommand != null) {
1268            removeCallbacks(mAdjustScrollerCommand);
1269        }
1270        if (mSetSelectionCommand != null) {
1271            removeCallbacks(mSetSelectionCommand);
1272        }
1273    }
1274
1275    /**
1276     * @return The selected index given its displayed <code>value</code>.
1277     */
1278    private int getSelectedPos(String value) {
1279        if (mDisplayedValues == null) {
1280            try {
1281                return Integer.parseInt(value);
1282            } catch (NumberFormatException e) {
1283                // Ignore as if it's not a number we don't care
1284            }
1285        } else {
1286            for (int i = 0; i < mDisplayedValues.length; i++) {
1287                // Don't force the user to type in jan when ja will do
1288                value = value.toLowerCase();
1289                if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1290                    return mStart + i;
1291                }
1292            }
1293
1294            /*
1295             * The user might have typed in a number into the month field i.e.
1296             * 10 instead of OCT so support that too.
1297             */
1298            try {
1299                return Integer.parseInt(value);
1300            } catch (NumberFormatException e) {
1301
1302                // Ignore as if it's not a number we don't care
1303            }
1304        }
1305        return mStart;
1306    }
1307
1308    /**
1309     * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1310     * </code> to
1311     * <code>selectionEnd</code>.
1312     */
1313    private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1314        if (mSetSelectionCommand == null) {
1315            mSetSelectionCommand = new SetSelectionCommand();
1316        } else {
1317            removeCallbacks(mSetSelectionCommand);
1318        }
1319        mSetSelectionCommand.mSelectionStart = selectionStart;
1320        mSetSelectionCommand.mSelectionEnd = selectionEnd;
1321        post(mSetSelectionCommand);
1322    }
1323
1324    /**
1325     * Posts an {@link AdjustScrollerCommand} within the given <code>
1326     * delayMillis</code>
1327     * .
1328     */
1329    private void postAdjustScrollerCommand(int delayMillis) {
1330        if (mAdjustScrollerCommand == null) {
1331            mAdjustScrollerCommand = new AdjustScrollerCommand();
1332        } else {
1333            removeCallbacks(mAdjustScrollerCommand);
1334        }
1335        postDelayed(mAdjustScrollerCommand, delayMillis);
1336    }
1337
1338    /**
1339     * Filter for accepting only valid indices or prefixes of the string
1340     * representation of valid indices.
1341     */
1342    class InputTextFilter extends NumberKeyListener {
1343
1344        // XXX This doesn't allow for range limits when controlled by a
1345        // soft input method!
1346        public int getInputType() {
1347            return InputType.TYPE_CLASS_TEXT;
1348        }
1349
1350        @Override
1351        protected char[] getAcceptedChars() {
1352            return DIGIT_CHARACTERS;
1353        }
1354
1355        @Override
1356        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
1357                int dstart, int dend) {
1358            if (mDisplayedValues == null) {
1359                CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1360                if (filtered == null) {
1361                    filtered = source.subSequence(start, end);
1362                }
1363
1364                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1365                        + dest.subSequence(dend, dest.length());
1366
1367                if ("".equals(result)) {
1368                    return result;
1369                }
1370                int val = getSelectedPos(result);
1371
1372                /*
1373                 * Ensure the user can't type in a value greater than the max
1374                 * allowed. We have to allow less than min as the user might
1375                 * want to delete some numbers and then type a new number.
1376                 */
1377                if (val > mEnd) {
1378                    return "";
1379                } else {
1380                    return filtered;
1381                }
1382            } else {
1383                CharSequence filtered = String.valueOf(source.subSequence(start, end));
1384                if (TextUtils.isEmpty(filtered)) {
1385                    return "";
1386                }
1387                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1388                        + dest.subSequence(dend, dest.length());
1389                String str = String.valueOf(result).toLowerCase();
1390                for (String val : mDisplayedValues) {
1391                    String valLowerCase = val.toLowerCase();
1392                    if (valLowerCase.startsWith(str)) {
1393                        postSetSelectionCommand(result.length(), val.length());
1394                        return val.subSequence(dstart, val.length());
1395                    }
1396                }
1397                return "";
1398            }
1399        }
1400    }
1401
1402    /**
1403     * Command for setting the input text selection.
1404     */
1405    class SetSelectionCommand implements Runnable {
1406        private int mSelectionStart;
1407
1408        private int mSelectionEnd;
1409
1410        public void run() {
1411            mInputText.setSelection(mSelectionStart, mSelectionEnd);
1412        }
1413    }
1414
1415    /**
1416     * Command for adjusting the scroller to show in its center the closest of
1417     * the displayed items.
1418     */
1419    class AdjustScrollerCommand implements Runnable {
1420        public void run() {
1421            mPreviousScrollerY = 0;
1422            int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
1423            float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize;
1424            int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS);
1425            mAdjustScroller.startScroll(0, 0, 0, deltaY, duration);
1426            invalidate();
1427        }
1428    }
1429
1430    /**
1431     * Command for updating the current value from a long press.
1432     */
1433    class UpdateValueFromLongPressCommand implements Runnable {
1434        private int mUpdateStep = 0;
1435
1436        private void setUpdateStep(int updateStep) {
1437            mUpdateStep = updateStep;
1438        }
1439
1440        public void run() {
1441            changeCurrent(mCurrent + mUpdateStep);
1442            postDelayed(this, mLongPressUpdateSpeed);
1443        }
1444    }
1445}
1446