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