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