NumberPicker.java revision 99b539f6326a5f0df123b34766becec023bf7a84
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());
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    }
802
803    /**
804     * Set the range of numbers allowed for the number picker. The current value
805     * will be automatically set to the start.
806     *
807     * @param start the start of the range (inclusive)
808     * @param end the end of the range (inclusive)
809     */
810    public void setRange(int start, int end) {
811        setRange(start, end, null);
812    }
813
814    /**
815     * Set the range of numbers allowed for the number picker. The current value
816     * will be automatically set to the start. Also provide a mapping for values
817     * used to display to the user instead of the numbers in the range.
818     *
819     * @param start The start of the range (inclusive).
820     * @param end The end of the range (inclusive).
821     * @param displayedValues The values displayed to the user.
822     */
823    public void setRange(int start, int end, String[] displayedValues) {
824        boolean wrapSelector = (end - start) >= mSelectorIndices.length;
825        setRange(start, end, displayedValues, wrapSelector);
826    }
827
828    /**
829     * Set the range of numbers allowed for the number picker. The current value
830     * will be automatically set to the start. Also provide a mapping for values
831     * used to display to the user.
832     * <p>
833     * Note: The <code>wrapSelectorWheel</code> argument is ignored if the range
834     * (difference between <code>start</code> and <code>end</code>) us less than
835     * five since this is the number of values shown by the selector wheel.
836     * </p>
837     *
838     * @param start the start of the range (inclusive)
839     * @param end the end of the range (inclusive)
840     * @param displayedValues the values displayed to the user.
841     * @param wrapSelectorWheel Whether to wrap the selector wheel.
842     *
843     * @see #setWrapSelectorWheel(boolean)
844     */
845    public void setRange(int start, int end, String[] displayedValues, boolean wrapSelectorWheel) {
846        if (start == mStart && end == mEnd) {
847            return;
848        }
849
850        if (start < 0 || end < 0) {
851            throw new IllegalArgumentException("start and end must be > 0");
852        }
853
854        mDisplayedValues = displayedValues;
855        mStart = start;
856        mEnd = end;
857        mCurrent = start;
858
859        setWrapSelectorWheel(wrapSelectorWheel);
860        updateInputTextView();
861
862        if (displayedValues != null) {
863            // Allow text entry rather than strictly numeric entry.
864            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
865                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
866        } else {
867            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
868        }
869
870        resetSelectorIndices();
871    }
872
873    /**
874     * Set the current value for the number picker.
875     *
876     * @param current the current value the start of the range (inclusive)
877     *
878     * @throws IllegalArgumentException when current is not within the range of
879     *             of the number picker.
880     */
881    public void setCurrent(int current) {
882        if (mCurrent == current) {
883            return;
884        }
885        if (current < mStart || current > mEnd) {
886            throw new IllegalArgumentException("current should be >= start and <= end");
887        }
888        mCurrent = current;
889        updateInputTextView();
890        updateIncrementAndDecrementButtonsVisibilityState();
891    }
892
893    /**
894     * Sets whether the selector wheel shown during flinging/scrolling should wrap
895     * around the beginning and end values. By default if the range is more than
896     * five (the number of items shown on the selector wheel) the selector wheel
897     * wrapping is enabled.
898     *
899     * @param wrapSelector Whether to wrap.
900     */
901    public void setWrapSelectorWheel(boolean wrapSelector) {
902        if (wrapSelector && (mEnd - mStart) < mSelectorIndices.length) {
903            throw new IllegalStateException("Range less than selector items count.");
904        }
905        if (wrapSelector != mWrapSelector) {
906            // force the selector indices array to be reinitialized
907            mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE;
908            mWrapSelector = wrapSelector;
909        }
910    }
911
912    /**
913     * Sets the speed at which the numbers be incremented and decremented when
914     * the up and down buttons are long pressed respectively.
915     *
916     * @param intervalMillis The speed (in milliseconds) at which the numbers
917     *            will be incremented and decremented (default 300ms).
918     */
919    public void setOnLongPressUpdateInterval(long intervalMillis) {
920        mLongPressUpdateInterval = intervalMillis;
921    }
922
923    /**
924     * Returns the current value of the NumberPicker.
925     *
926     * @return the current value.
927     */
928    public int getCurrent() {
929        return mCurrent;
930    }
931
932    /**
933     * Returns the range lower value of the NumberPicker.
934     *
935     * @return The lower number of the range.
936     */
937    public int getRangeStart() {
938        return mStart;
939    }
940
941    /**
942     * Returns the range end value of the NumberPicker.
943     *
944     * @return The upper number of the range.
945     */
946    public int getRangeEnd() {
947        return mEnd;
948    }
949
950    @Override
951    protected float getTopFadingEdgeStrength() {
952        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
953    }
954
955    @Override
956    protected float getBottomFadingEdgeStrength() {
957        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
958    }
959
960    @Override
961    protected void onDetachedFromWindow() {
962        removeAllCallbacks();
963    }
964
965    @Override
966    protected void dispatchDraw(Canvas canvas) {
967        // There is a good reason for doing this. See comments in draw().
968    }
969
970    @Override
971    public void draw(Canvas canvas) {
972        // Dispatch draw to our children only if we are not currently running
973        // the animation for simultaneously fading out the scroll wheel and
974        // showing in the buttons. This class takes advantage of the View
975        // implementation of fading edges effect to draw the selector wheel.
976        // However, in View.draw(), the fading is applied after all the children
977        // have been drawn and we do not want this fading to be applied to the
978        // buttons which are currently showing in. Therefore, we draw our
979        // children
980        // after we have completed drawing ourselves.
981
982        // Draw the selector wheel if needed
983        if (mDrawSelectorWheel) {
984            super.draw(canvas);
985        }
986
987        // Draw our children if we are not showing the selector wheel of fading
988        // it out
989        if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) {
990            long drawTime = getDrawingTime();
991            for (int i = 0, count = getChildCount(); i < count; i++) {
992                View child = getChildAt(i);
993                if (!child.isShown()) {
994                    continue;
995                }
996                drawChild(canvas, getChildAt(i), drawTime);
997            }
998        }
999    }
1000
1001    @Override
1002    protected void onDraw(Canvas canvas) {
1003        // we only draw the selector wheel
1004        if (!mDrawSelectorWheel) {
1005            return;
1006        }
1007        float x = (mRight - mLeft) / 2;
1008        float y = mCurrentScrollOffset;
1009
1010        int[] selectorIndices = getSelectorIndices();
1011        for (int i = 0; i < selectorIndices.length; i++) {
1012            int selectorIndex = selectorIndices[i];
1013            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1014            canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint);
1015            y += mSelectorElementHeight;
1016        }
1017    }
1018
1019    /**
1020     * Resets the selector indices and clear the cached
1021     * string representation of these indices.
1022     */
1023    private void resetSelectorIndices() {
1024        mSelectorIndexToStringCache.clear();
1025        int[] selectorIdices = getSelectorIndices();
1026        for (int i = 0; i < selectorIdices.length; i++) {
1027            selectorIdices[i] = Integer.MIN_VALUE;
1028        }
1029    }
1030
1031    /**
1032     * Sets the current value of this NumberPicker, and sets mPrevious to the
1033     * previous value. If current is greater than mEnd less than mStart, the
1034     * value of mCurrent is wrapped around. Subclasses can override this to
1035     * change the wrapping behavior
1036     *
1037     * @param current the new value of the NumberPicker
1038     */
1039    private void changeCurrent(int current) {
1040        if (mCurrent == current) {
1041            return;
1042        }
1043        // Wrap around the values if we go past the start or end
1044        if (mWrapSelector) {
1045            current = getWrappedSelectorIndex(current);
1046        }
1047        int previous = mCurrent;
1048        setCurrent(current);
1049        notifyChange(previous, current);
1050    }
1051
1052    /**
1053     * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
1054     * wheel.
1055     */
1056    @SuppressWarnings("unused")
1057    // Called by ShowInputControlsAnimator via reflection
1058    private void setSelectorPaintAlpha(int alpha) {
1059        mSelectorPaint.setAlpha(alpha);
1060        if (mDrawSelectorWheel) {
1061            invalidate();
1062        }
1063    }
1064
1065    /**
1066     * @return If the <code>event</code> is in the input text.
1067     */
1068    private boolean isEventInInputText(MotionEvent event) {
1069        mInputText.getHitRect(mTempRect);
1070        return mTempRect.contains((int) event.getX(), (int) event.getY());
1071    }
1072
1073    /**
1074     * Sets if to <code>drawSelectionWheel</code>.
1075     */
1076    private void setDrawSelectorWheel(boolean drawSelectorWheel) {
1077        mDrawSelectorWheel = drawSelectorWheel;
1078        // do not fade if the selector wheel not shown
1079        setVerticalFadingEdgeEnabled(drawSelectorWheel);
1080    }
1081
1082    /**
1083     * Callback invoked upon completion of a given <code>scroller</code>.
1084     */
1085    private void onScrollerFinished(Scroller scroller) {
1086        if (scroller == mFlingScroller) {
1087            postAdjustScrollerCommand(0);
1088            tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE);
1089        } else {
1090            showInputControls();
1091            updateInputTextView();
1092        }
1093    }
1094
1095    /**
1096     * Notifies the scroll listener for the given <code>scrollState</code>
1097     * if the scroll state differs from the current scroll state.
1098     */
1099    private void tryNotifyScrollListener(int scrollState) {
1100        if (mOnScrollListener != null && mScrollState != scrollState) {
1101            mScrollState = scrollState;
1102            mOnScrollListener.onScrollStateChange(this, scrollState);
1103        }
1104    }
1105
1106    /**
1107     * Flings the selector with the given <code>velocityY</code>.
1108     */
1109    private void fling(int velocityY) {
1110        mPreviousScrollerY = 0;
1111        Scroller flingScroller = mFlingScroller;
1112
1113        if (mWrapSelector) {
1114            if (velocityY > 0) {
1115                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1116            } else {
1117                flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1118            }
1119        } else {
1120            if (velocityY > 0) {
1121                int maxY = mTextSize * (mCurrent - mStart);
1122                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY);
1123            } else {
1124                int startY = mTextSize * (mEnd - mCurrent);
1125                int maxY = startY;
1126                flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY);
1127            }
1128        }
1129
1130        postAdjustScrollerCommand(flingScroller.getDuration());
1131        invalidate();
1132    }
1133
1134    /**
1135     * Hides the input controls which is the up/down arrows and the text field.
1136     */
1137    private void hideInputControls() {
1138        mShowInputControlsAnimator.cancel();
1139        mIncrementButton.setVisibility(INVISIBLE);
1140        mDecrementButton.setVisibility(INVISIBLE);
1141        mInputText.setVisibility(INVISIBLE);
1142    }
1143
1144    /**
1145     * Show the input controls by making them visible and animating the alpha
1146     * property up/down arrows.
1147     */
1148    private void showInputControls() {
1149        updateIncrementAndDecrementButtonsVisibilityState();
1150        mInputText.setVisibility(VISIBLE);
1151        mShowInputControlsAnimator.start();
1152    }
1153
1154    /**
1155     * Updates the visibility state of the increment and decrement buttons.
1156     */
1157    private void updateIncrementAndDecrementButtonsVisibilityState() {
1158        if (mWrapSelector || mCurrent < mEnd) {
1159            mIncrementButton.setVisibility(VISIBLE);
1160        } else {
1161            mIncrementButton.setVisibility(INVISIBLE);
1162        }
1163        if (mWrapSelector || mCurrent > mStart) {
1164            mDecrementButton.setVisibility(VISIBLE);
1165        } else {
1166            mDecrementButton.setVisibility(INVISIBLE);
1167        }
1168    }
1169
1170    /**
1171     * @return The selector indices array with proper values with the current as
1172     *         the middle one.
1173     */
1174    private int[] getSelectorIndices() {
1175        int current = getCurrent();
1176        if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) {
1177            for (int i = 0; i < mSelectorIndices.length; i++) {
1178                int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1179                if (mWrapSelector) {
1180                    selectorIndex = getWrappedSelectorIndex(selectorIndex);
1181                }
1182                mSelectorIndices[i] = selectorIndex;
1183                ensureCachedScrollSelectorValue(mSelectorIndices[i]);
1184            }
1185        }
1186        return mSelectorIndices;
1187    }
1188
1189    /**
1190     * @return The wrapped index <code>selectorIndex</code> value.
1191     */
1192    private int getWrappedSelectorIndex(int selectorIndex) {
1193        if (selectorIndex > mEnd) {
1194            return mStart + (selectorIndex - mEnd) % (mEnd - mStart);
1195        } else if (selectorIndex < mStart) {
1196            return mEnd - (mStart - selectorIndex) % (mEnd - mStart);
1197        }
1198        return selectorIndex;
1199    }
1200
1201    /**
1202     * Increments the <code>selectorIndices</code> whose string representations
1203     * will be displayed in the selector.
1204     */
1205    private void incrementScrollSelectorIndices(int[] selectorIndices) {
1206        for (int i = 0; i < selectorIndices.length - 1; i++) {
1207            selectorIndices[i] = selectorIndices[i + 1];
1208        }
1209        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1210        if (mWrapSelector && nextScrollSelectorIndex > mEnd) {
1211            nextScrollSelectorIndex = mStart;
1212        }
1213        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1214        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1215    }
1216
1217    /**
1218     * Decrements the <code>selectorIndices</code> whose string representations
1219     * will be displayed in the selector.
1220     */
1221    private void decrementSelectorIndices(int[] selectorIndices) {
1222        for (int i = selectorIndices.length - 1; i > 0; i--) {
1223            selectorIndices[i] = selectorIndices[i - 1];
1224        }
1225        int nextScrollSelectorIndex = selectorIndices[1] - 1;
1226        if (mWrapSelector && nextScrollSelectorIndex < mStart) {
1227            nextScrollSelectorIndex = mEnd;
1228        }
1229        selectorIndices[0] = nextScrollSelectorIndex;
1230        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1231    }
1232
1233    /**
1234     * Ensures we have a cached string representation of the given <code>
1235     * selectorIndex</code>
1236     * to avoid multiple instantiations of the same string.
1237     */
1238    private void ensureCachedScrollSelectorValue(int selectorIndex) {
1239        SparseArray<String> cache = mSelectorIndexToStringCache;
1240        String scrollSelectorValue = cache.get(selectorIndex);
1241        if (scrollSelectorValue != null) {
1242            return;
1243        }
1244        if (selectorIndex < mStart || selectorIndex > mEnd) {
1245            scrollSelectorValue = "";
1246        } else {
1247            if (mDisplayedValues != null) {
1248                int displayedValueIndex = selectorIndex - mStart;
1249                scrollSelectorValue = mDisplayedValues[displayedValueIndex];
1250            } else {
1251                scrollSelectorValue = formatNumber(selectorIndex);
1252            }
1253        }
1254        cache.put(selectorIndex, scrollSelectorValue);
1255    }
1256
1257    private String formatNumber(int value) {
1258        return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
1259    }
1260
1261    private void validateInputTextView(View v) {
1262        String str = String.valueOf(((TextView) v).getText());
1263        if (TextUtils.isEmpty(str)) {
1264            // Restore to the old value as we don't allow empty values
1265            updateInputTextView();
1266        } else {
1267            // Check the new value and ensure it's in range
1268            int current = getSelectedPos(str.toString());
1269            changeCurrent(current);
1270        }
1271    }
1272
1273    /**
1274     * Updates the view of this NumberPicker. If displayValues were specified in
1275     * {@link #setRange}, the string corresponding to the index specified by the
1276     * current value will be returned. Otherwise, the formatter specified in
1277     * {@link #setFormatter} will be used to format the number.
1278     */
1279    private void updateInputTextView() {
1280        /*
1281         * If we don't have displayed values then use the current number else
1282         * find the correct value in the displayed values for the current
1283         * number.
1284         */
1285        if (mDisplayedValues == null) {
1286            mInputText.setText(formatNumber(mCurrent));
1287        } else {
1288            mInputText.setText(mDisplayedValues[mCurrent - mStart]);
1289        }
1290        mInputText.setSelection(mInputText.getText().length());
1291    }
1292
1293    /**
1294     * Notifies the listener, if registered, of a change of the value of this
1295     * NumberPicker.
1296     */
1297    private void notifyChange(int previous, int current) {
1298        if (mOnChangeListener != null) {
1299            mOnChangeListener.onChange(this, previous, mCurrent);
1300        }
1301    }
1302
1303    /**
1304     * Posts a command for updating the current value every <code>updateMillis
1305     * </code>.
1306     */
1307    private void postUpdateValueFromLongPress(int updateMillis) {
1308        mInputText.clearFocus();
1309        removeAllCallbacks();
1310        if (mUpdateFromLongPressCommand == null) {
1311            mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand();
1312        }
1313        mUpdateFromLongPressCommand.setUpdateStep(updateMillis);
1314        post(mUpdateFromLongPressCommand);
1315    }
1316
1317    /**
1318     * Removes all pending callback from the message queue.
1319     */
1320    private void removeAllCallbacks() {
1321        if (mUpdateFromLongPressCommand != null) {
1322            removeCallbacks(mUpdateFromLongPressCommand);
1323        }
1324        if (mAdjustScrollerCommand != null) {
1325            removeCallbacks(mAdjustScrollerCommand);
1326        }
1327        if (mSetSelectionCommand != null) {
1328            removeCallbacks(mSetSelectionCommand);
1329        }
1330    }
1331
1332    /**
1333     * @return The selected index given its displayed <code>value</code>.
1334     */
1335    private int getSelectedPos(String value) {
1336        if (mDisplayedValues == null) {
1337            try {
1338                return Integer.parseInt(value);
1339            } catch (NumberFormatException e) {
1340                // Ignore as if it's not a number we don't care
1341            }
1342        } else {
1343            for (int i = 0; i < mDisplayedValues.length; i++) {
1344                // Don't force the user to type in jan when ja will do
1345                value = value.toLowerCase();
1346                if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1347                    return mStart + i;
1348                }
1349            }
1350
1351            /*
1352             * The user might have typed in a number into the month field i.e.
1353             * 10 instead of OCT so support that too.
1354             */
1355            try {
1356                return Integer.parseInt(value);
1357            } catch (NumberFormatException e) {
1358
1359                // Ignore as if it's not a number we don't care
1360            }
1361        }
1362        return mStart;
1363    }
1364
1365    /**
1366     * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1367     * </code> to
1368     * <code>selectionEnd</code>.
1369     */
1370    private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1371        if (mSetSelectionCommand == null) {
1372            mSetSelectionCommand = new SetSelectionCommand();
1373        } else {
1374            removeCallbacks(mSetSelectionCommand);
1375        }
1376        mSetSelectionCommand.mSelectionStart = selectionStart;
1377        mSetSelectionCommand.mSelectionEnd = selectionEnd;
1378        post(mSetSelectionCommand);
1379    }
1380
1381    /**
1382     * Posts an {@link AdjustScrollerCommand} within the given <code>
1383     * delayMillis</code>
1384     * .
1385     */
1386    private void postAdjustScrollerCommand(int delayMillis) {
1387        if (mAdjustScrollerCommand == null) {
1388            mAdjustScrollerCommand = new AdjustScrollerCommand();
1389        } else {
1390            removeCallbacks(mAdjustScrollerCommand);
1391        }
1392        postDelayed(mAdjustScrollerCommand, delayMillis);
1393    }
1394
1395    /**
1396     * Filter for accepting only valid indices or prefixes of the string
1397     * representation of valid indices.
1398     */
1399    class InputTextFilter extends NumberKeyListener {
1400
1401        // XXX This doesn't allow for range limits when controlled by a
1402        // soft input method!
1403        public int getInputType() {
1404            return InputType.TYPE_CLASS_TEXT;
1405        }
1406
1407        @Override
1408        protected char[] getAcceptedChars() {
1409            return DIGIT_CHARACTERS;
1410        }
1411
1412        @Override
1413        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
1414                int dstart, int dend) {
1415            if (mDisplayedValues == null) {
1416                CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1417                if (filtered == null) {
1418                    filtered = source.subSequence(start, end);
1419                }
1420
1421                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1422                        + dest.subSequence(dend, dest.length());
1423
1424                if ("".equals(result)) {
1425                    return result;
1426                }
1427                int val = getSelectedPos(result);
1428
1429                /*
1430                 * Ensure the user can't type in a value greater than the max
1431                 * allowed. We have to allow less than min as the user might
1432                 * want to delete some numbers and then type a new number.
1433                 */
1434                if (val > mEnd) {
1435                    return "";
1436                } else {
1437                    return filtered;
1438                }
1439            } else {
1440                CharSequence filtered = String.valueOf(source.subSequence(start, end));
1441                if (TextUtils.isEmpty(filtered)) {
1442                    return "";
1443                }
1444                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1445                        + dest.subSequence(dend, dest.length());
1446                String str = String.valueOf(result).toLowerCase();
1447                for (String val : mDisplayedValues) {
1448                    String valLowerCase = val.toLowerCase();
1449                    if (valLowerCase.startsWith(str)) {
1450                        postSetSelectionCommand(result.length(), val.length());
1451                        return val.subSequence(dstart, val.length());
1452                    }
1453                }
1454                return "";
1455            }
1456        }
1457    }
1458
1459    /**
1460     * Command for setting the input text selection.
1461     */
1462    class SetSelectionCommand implements Runnable {
1463        private int mSelectionStart;
1464
1465        private int mSelectionEnd;
1466
1467        public void run() {
1468            mInputText.setSelection(mSelectionStart, mSelectionEnd);
1469        }
1470    }
1471
1472    /**
1473     * Command for adjusting the scroller to show in its center the closest of
1474     * the displayed items.
1475     */
1476    class AdjustScrollerCommand implements Runnable {
1477        public void run() {
1478            mPreviousScrollerY = 0;
1479            int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
1480            float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize;
1481            int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS);
1482            mAdjustScroller.startScroll(0, 0, 0, deltaY, duration);
1483            invalidate();
1484        }
1485    }
1486
1487    /**
1488     * Command for updating the current value from a long press.
1489     */
1490    class UpdateValueFromLongPressCommand implements Runnable {
1491        private int mUpdateStep = 0;
1492
1493        private void setUpdateStep(int updateStep) {
1494            mUpdateStep = updateStep;
1495        }
1496
1497        public void run() {
1498            changeCurrent(mCurrent + mUpdateStep);
1499            postDelayed(this, mLongPressUpdateInterval);
1500        }
1501    }
1502}
1503