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