NumberPicker.java revision 9cd5fb2f46e219ec8250383fb201b94cb2ee8c0a
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 OnValueChangedListener mOnValueChangedListener;
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 OnValueChangedListener {
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 (isEventInInputText(event)) {
627                    mAdjustScrollerOnUpEvent = false;
628                    setDrawSelectorWheel(true);
629                    hideInputControls();
630                    return true;
631                }
632                break;
633            case MotionEvent.ACTION_MOVE:
634                float currentMoveY = event.getY();
635                int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
636                if (deltaDownY > mTouchSlop) {
637                    mBeginEditOnUpEvent = false;
638                    onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
639                    setDrawSelectorWheel(true);
640                    hideInputControls();
641                    return true;
642                }
643                break;
644        }
645        return false;
646    }
647
648    @Override
649    public boolean onTouchEvent(MotionEvent ev) {
650        if (!isEnabled()) {
651            return false;
652        }
653        if (mVelocityTracker == null) {
654            mVelocityTracker = VelocityTracker.obtain();
655        }
656        mVelocityTracker.addMovement(ev);
657        int action = ev.getActionMasked();
658        switch (action) {
659            case MotionEvent.ACTION_MOVE:
660                float currentMoveY = ev.getY();
661                if (mBeginEditOnUpEvent
662                        || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
663                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
664                    if (deltaDownY > mTouchSlop) {
665                        mBeginEditOnUpEvent = false;
666                        onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
667                    }
668                }
669                int deltaMoveY = (int) (currentMoveY - mLastMotionEventY);
670                scrollBy(0, deltaMoveY);
671                invalidate();
672                mLastMotionEventY = currentMoveY;
673                break;
674            case MotionEvent.ACTION_UP:
675                if (mBeginEditOnUpEvent) {
676                    setDrawSelectorWheel(false);
677                    showInputControls(mShowInputControlsAnimimationDuration);
678                    mInputText.requestFocus();
679                    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
680                            Context.INPUT_METHOD_SERVICE);
681                    imm.showSoftInput(mInputText, 0);
682                    mInputText.setSelection(0, mInputText.getText().length());
683                    return true;
684                }
685                VelocityTracker velocityTracker = mVelocityTracker;
686                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
687                int initialVelocity = (int) velocityTracker.getYVelocity();
688                if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
689                    fling(initialVelocity);
690                    onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
691                } else {
692                    if (mAdjustScrollerOnUpEvent) {
693                        if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) {
694                            postAdjustScrollerCommand(0);
695                        }
696                    } else {
697                        postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS);
698                    }
699                }
700                mVelocityTracker.recycle();
701                mVelocityTracker = null;
702                break;
703        }
704        return true;
705    }
706
707    @Override
708    public boolean dispatchTouchEvent(MotionEvent event) {
709        int action = event.getActionMasked();
710        if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)
711                && !isEventInInputText(event)) {
712            removeAllCallbacks();
713        }
714        return super.dispatchTouchEvent(event);
715    }
716
717    @Override
718    public boolean dispatchKeyEvent(KeyEvent event) {
719        int keyCode = event.getKeyCode();
720        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
721            removeAllCallbacks();
722        }
723        return super.dispatchKeyEvent(event);
724    }
725
726    @Override
727    public boolean dispatchTrackballEvent(MotionEvent event) {
728        int action = event.getActionMasked();
729        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
730            removeAllCallbacks();
731        }
732        return super.dispatchTrackballEvent(event);
733    }
734
735    @Override
736    public void computeScroll() {
737        if (!mDrawSelectorWheel) {
738            return;
739        }
740        Scroller scroller = mFlingScroller;
741        if (scroller.isFinished()) {
742            scroller = mAdjustScroller;
743            if (scroller.isFinished()) {
744                return;
745            }
746        }
747        scroller.computeScrollOffset();
748        int currentScrollerY = scroller.getCurrY();
749        if (mPreviousScrollerY == 0) {
750            mPreviousScrollerY = scroller.getStartY();
751        }
752        scrollBy(0, currentScrollerY - mPreviousScrollerY);
753        mPreviousScrollerY = currentScrollerY;
754        if (scroller.isFinished()) {
755            onScrollerFinished(scroller);
756        } else {
757            invalidate();
758        }
759    }
760
761    @Override
762    public void setEnabled(boolean enabled) {
763        super.setEnabled(enabled);
764        mIncrementButton.setEnabled(enabled);
765        mDecrementButton.setEnabled(enabled);
766        mInputText.setEnabled(enabled);
767    }
768
769    @Override
770    public void scrollBy(int x, int y) {
771        int[] selectorIndices = getSelectorIndices();
772        if (!mWrapSelectorWheel && y > 0
773                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
774            mCurrentScrollOffset = mInitialScrollOffset;
775            return;
776        }
777        if (!mWrapSelectorWheel && y < 0
778                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
779            mCurrentScrollOffset = mInitialScrollOffset;
780            return;
781        }
782        mCurrentScrollOffset += y;
783        while (mCurrentScrollOffset - mInitialScrollOffset >= mSelectorElementHeight) {
784            mCurrentScrollOffset -= mSelectorElementHeight;
785            decrementSelectorIndices(selectorIndices);
786            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
787            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
788                mCurrentScrollOffset = mInitialScrollOffset;
789            }
790        }
791        while (mCurrentScrollOffset - mInitialScrollOffset <= -mSelectorElementHeight) {
792            mCurrentScrollOffset += mSelectorElementHeight;
793            incrementScrollSelectorIndices(selectorIndices);
794            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
795            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
796                mCurrentScrollOffset = mInitialScrollOffset;
797            }
798        }
799    }
800
801    @Override
802    public int getSolidColor() {
803        return mSolidColor;
804    }
805
806    /**
807     * Sets the listener to be notified on change of the current value.
808     *
809     * @param onValueChangedListener The listener.
810     */
811    public void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) {
812        mOnValueChangedListener = onValueChangedListener;
813    }
814
815    /**
816     * Set listener to be notified for scroll state changes.
817     *
818     * @param onScrollListener The listener.
819     */
820    public void setOnScrollListener(OnScrollListener onScrollListener) {
821        mOnScrollListener = onScrollListener;
822    }
823
824    /**
825     * Set the formatter to be used for formatting the current value.
826     * <p>
827     * Note: If you have provided alternative values for the values this
828     * formatter is never invoked.
829     * </p>
830     *
831     * @param formatter The formatter object. If formatter is <code>null</code>,
832     *            {@link String#valueOf(int)} will be used.
833     *
834     * @see #setDisplayedValues(String[])
835     */
836    public void setFormatter(Formatter formatter) {
837        if (formatter == mFormatter) {
838            return;
839        }
840        mFormatter = formatter;
841        resetSelectorWheelIndices();
842        updateInputTextView();
843    }
844
845    /**
846     * Set the current value for the number picker.
847     * <p>
848     * If the argument is less than the {@link NumberPicker#getMinValue()} and
849     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
850     * current value is set to the {@link NumberPicker#getMinValue()} value.
851     * </p>
852     * <p>
853     * If the argument is less than the {@link NumberPicker#getMinValue()} and
854     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
855     * current value is set to the {@link NumberPicker#getMaxValue()} value.
856     * </p>
857     * <p>
858     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
859     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
860     * current value is set to the {@link NumberPicker#getMaxValue()} value.
861     * </p>
862     * <p>
863     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
864     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
865     * current value is set to the {@link NumberPicker#getMinValue()} value.
866     * </p>
867     *
868     * @param value The current value.
869     * @see #setWrapSelectorWheel(boolean)
870     * @see #setMinValue(int)
871     * @see #setMaxValue(int)
872     */
873    public void setValue(int value) {
874        if (mValue == value) {
875            return;
876        }
877        if (value < mMinValue) {
878            value = mWrapSelectorWheel ? mMaxValue : mMinValue;
879        }
880        if (value > mMaxValue) {
881            value = mWrapSelectorWheel ? mMinValue : mMaxValue;
882        }
883        mValue = value;
884        updateInputTextView();
885        updateIncrementAndDecrementButtonsVisibilityState();
886    }
887
888    /**
889     * Gets whether the selector wheel wraps when reaching the min/max value.
890     *
891     * @return True if the selector wheel wraps.
892     *
893     * @see #getMinValue()
894     * @see #getMaxValue()
895     */
896    public boolean getWrapSelectorWheel() {
897        return mWrapSelectorWheel;
898    }
899
900    /**
901     * Sets whether the selector wheel shown during flinging/scrolling should
902     * wrap around the {@link NumberPicker#getMinValue()} and
903     * {@link NumberPicker#getMaxValue()} values.
904     * <p>
905     * By default if the range (max - min) is more than five (the number of
906     * items shown on the selector wheel) the selector wheel wrapping is
907     * enabled.
908     * </p>
909     *
910     * @param wrapSelector Whether to wrap.
911     */
912    public void setWrapSelectorWheel(boolean wrapSelector) {
913        if (wrapSelector && (mMaxValue - mMinValue) < mSelectorIndices.length) {
914            throw new IllegalStateException("Range less than selector items count.");
915        }
916        if (wrapSelector != mWrapSelectorWheel) {
917            // force the selector indices array to be reinitialized
918            mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE;
919            mWrapSelectorWheel = wrapSelector;
920        }
921    }
922
923    /**
924     * Sets the speed at which the numbers be incremented and decremented when
925     * the up and down buttons are long pressed respectively.
926     * <p>
927     * The default value is 300 ms.
928     * </p>
929     *
930     * @param intervalMillis The speed (in milliseconds) at which the numbers
931     *            will be incremented and decremented.
932     */
933    public void setOnLongPressUpdateInterval(long intervalMillis) {
934        mLongPressUpdateInterval = intervalMillis;
935    }
936
937    /**
938     * Returns the value of the picker.
939     *
940     * @return The value.
941     */
942    public int getValue() {
943        return mValue;
944    }
945
946    /**
947     * Returns the min value of the picker.
948     *
949     * @return The min value
950     */
951    public int getMinValue() {
952        return mMinValue;
953    }
954
955    /**
956     * Sets the min value of the picker.
957     *
958     * @param minValue The min value.
959     */
960    public void setMinValue(int minValue) {
961        if (mMinValue == minValue) {
962            return;
963        }
964        if (minValue < 0) {
965            throw new IllegalArgumentException("minValue must be >= 0");
966        }
967        mMinValue = minValue;
968        if (mMinValue > mValue) {
969            mValue = mMinValue;
970        }
971        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
972        setWrapSelectorWheel(wrapSelectorWheel);
973        resetSelectorWheelIndices();
974        updateInputTextView();
975        updateIncrementAndDecrementButtonsVisibilityState();
976    }
977
978    /**
979     * Returns the max value of the picker.
980     *
981     * @return The max value.
982     */
983    public int getMaxValue() {
984        return mMaxValue;
985    }
986
987    /**
988     * Sets the max value of the picker.
989     *
990     * @param maxValue The max value.
991     */
992    public void setMaxValue(int maxValue) {
993        if (mMaxValue == maxValue) {
994            return;
995        }
996        if (maxValue < 0) {
997            throw new IllegalArgumentException("maxValue must be >= 0");
998        }
999        mMaxValue = maxValue;
1000        if (mMaxValue < mValue) {
1001            mValue = mMaxValue;
1002        }
1003        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1004        setWrapSelectorWheel(wrapSelectorWheel);
1005        resetSelectorWheelIndices();
1006        updateInputTextView();
1007        updateIncrementAndDecrementButtonsVisibilityState();
1008    }
1009
1010    /**
1011     * Gets the values to be displayed instead of string values.
1012     *
1013     * @return The displayed values.
1014     */
1015    public String[] getDisplayedValues() {
1016        return mDisplayedValues;
1017    }
1018
1019    /**
1020     * Sets the values to be displayed.
1021     *
1022     * @param displayedValues The displayed values.
1023     */
1024    public void setDisplayedValues(String[] displayedValues) {
1025        if (mDisplayedValues == displayedValues) {
1026            return;
1027        }
1028        mDisplayedValues = displayedValues;
1029        if (mDisplayedValues != null) {
1030            // Allow text entry rather than strictly numeric entry.
1031            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
1032                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
1033        } else {
1034            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1035        }
1036        updateInputTextView();
1037        resetSelectorWheelIndices();
1038    }
1039
1040    @Override
1041    protected float getTopFadingEdgeStrength() {
1042        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1043    }
1044
1045    @Override
1046    protected float getBottomFadingEdgeStrength() {
1047        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1048    }
1049
1050    @Override
1051    protected void onAttachedToWindow() {
1052        super.onAttachedToWindow();
1053        // make sure we show the controls only the very
1054        // first time the user sees this widget
1055        if (mFlingable) {
1056            // animate a bit slower the very first time
1057            showInputControls(mShowInputControlsAnimimationDuration * 2);
1058        }
1059    }
1060
1061    @Override
1062    protected void onDetachedFromWindow() {
1063        removeAllCallbacks();
1064    }
1065
1066    @Override
1067    protected void dispatchDraw(Canvas canvas) {
1068        // There is a good reason for doing this. See comments in draw().
1069    }
1070
1071    @Override
1072    public void draw(Canvas canvas) {
1073        // Dispatch draw to our children only if we are not currently running
1074        // the animation for simultaneously fading out the scroll wheel and
1075        // showing in the buttons. This class takes advantage of the View
1076        // implementation of fading edges effect to draw the selector wheel.
1077        // However, in View.draw(), the fading is applied after all the children
1078        // have been drawn and we do not want this fading to be applied to the
1079        // buttons which are currently showing in. Therefore, we draw our
1080        // children after we have completed drawing ourselves.
1081        super.draw(canvas);
1082
1083        // Draw our children if we are not showing the selector wheel of fading
1084        // it out
1085        if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) {
1086            long drawTime = getDrawingTime();
1087            for (int i = 0, count = getChildCount(); i < count; i++) {
1088                View child = getChildAt(i);
1089                if (!child.isShown()) {
1090                    continue;
1091                }
1092                drawChild(canvas, getChildAt(i), drawTime);
1093            }
1094        }
1095    }
1096
1097    @Override
1098    protected void onDraw(Canvas canvas) {
1099        // we only draw the selector wheel
1100        if (!mDrawSelectorWheel) {
1101            return;
1102        }
1103        float x = (mRight - mLeft) / 2;
1104        float y = mCurrentScrollOffset;
1105
1106        // draw the selector wheel
1107        int[] selectorIndices = getSelectorIndices();
1108        for (int i = 0; i < selectorIndices.length; i++) {
1109            int selectorIndex = selectorIndices[i];
1110            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1111            canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint);
1112            y += mSelectorElementHeight;
1113        }
1114
1115        // draw the selection dividers (only if scrolling and drawable specified)
1116        if (mSelectionDivider != null) {
1117            mSelectionDivider.setAlpha(mSelectorPaint.getAlpha());
1118            // draw the top divider
1119            int topOfTopDivider =
1120                (getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2;
1121            int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
1122            mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
1123            mSelectionDivider.draw(canvas);
1124
1125            // draw the bottom divider
1126            int topOfBottomDivider =  topOfTopDivider + mSelectorElementHeight;
1127            int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight;
1128            mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
1129            mSelectionDivider.draw(canvas);
1130        }
1131    }
1132
1133    /**
1134     * Resets the selector indices and clear the cached
1135     * string representation of these indices.
1136     */
1137    private void resetSelectorWheelIndices() {
1138        mSelectorIndexToStringCache.clear();
1139        int[] selectorIdices = getSelectorIndices();
1140        for (int i = 0; i < selectorIdices.length; i++) {
1141            selectorIdices[i] = Integer.MIN_VALUE;
1142        }
1143    }
1144
1145    /**
1146     * Sets the current value of this NumberPicker, and sets mPrevious to the
1147     * previous value. If current is greater than mEnd less than mStart, the
1148     * value of mCurrent is wrapped around. Subclasses can override this to
1149     * change the wrapping behavior
1150     *
1151     * @param current the new value of the NumberPicker
1152     */
1153    private void changeCurrent(int current) {
1154        if (mValue == current) {
1155            return;
1156        }
1157        // Wrap around the values if we go past the start or end
1158        if (mWrapSelectorWheel) {
1159            current = getWrappedSelectorIndex(current);
1160        }
1161        int previous = mValue;
1162        setValue(current);
1163        notifyChange(previous, current);
1164    }
1165
1166    /**
1167     * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
1168     * wheel.
1169     */
1170    @SuppressWarnings("unused")
1171    // Called by ShowInputControlsAnimator via reflection
1172    private void setSelectorPaintAlpha(int alpha) {
1173        mSelectorPaint.setAlpha(alpha);
1174        if (mDrawSelectorWheel) {
1175            invalidate();
1176        }
1177    }
1178
1179    /**
1180     * @return If the <code>event</code> is in the input text.
1181     */
1182    private boolean isEventInInputText(MotionEvent event) {
1183        mInputText.getHitRect(mTempRect);
1184        return mTempRect.contains((int) event.getX(), (int) event.getY());
1185    }
1186
1187    /**
1188     * Sets if to <code>drawSelectionWheel</code>.
1189     */
1190    private void setDrawSelectorWheel(boolean drawSelectorWheel) {
1191        mDrawSelectorWheel = drawSelectorWheel;
1192        // do not fade if the selector wheel not shown
1193        setVerticalFadingEdgeEnabled(drawSelectorWheel);
1194    }
1195
1196    private void initializeScrollWheel() {
1197        if (mInitialScrollOffset != Integer.MIN_VALUE) {
1198            return;
1199
1200        }
1201        int[] selectorIndices = getSelectorIndices();
1202        int totalTextHeight = selectorIndices.length * mTextSize;
1203        int totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
1204        int textGapCount = selectorIndices.length - 1;
1205        int selectorTextGapHeight = totalTextGapHeight / textGapCount;
1206        // compensate for integer division loss of the components used to
1207        // calculate the text gap
1208        int integerDivisionLoss = (mTextSize + mBottom - mTop) % textGapCount;
1209        mInitialScrollOffset = mCurrentScrollOffset = mTextSize - integerDivisionLoss / 2;
1210        mSelectorElementHeight = mTextSize + selectorTextGapHeight;
1211        updateInputTextView();
1212    }
1213
1214    /**
1215     * Callback invoked upon completion of a given <code>scroller</code>.
1216     */
1217    private void onScrollerFinished(Scroller scroller) {
1218        if (scroller == mFlingScroller) {
1219            postAdjustScrollerCommand(0);
1220            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1221        } else {
1222            updateInputTextView();
1223            showInputControls(mShowInputControlsAnimimationDuration);
1224        }
1225    }
1226
1227    /**
1228     * Handles transition to a given <code>scrollState</code>
1229     */
1230    private void onScrollStateChange(int scrollState) {
1231        if (mScrollState == scrollState) {
1232            return;
1233        }
1234        mScrollState = scrollState;
1235        if (mOnScrollListener != null) {
1236            mOnScrollListener.onScrollStateChange(this, scrollState);
1237        }
1238    }
1239
1240    /**
1241     * Flings the selector with the given <code>velocityY</code>.
1242     */
1243    private void fling(int velocityY) {
1244        mPreviousScrollerY = 0;
1245        Scroller flingScroller = mFlingScroller;
1246
1247        if (mWrapSelectorWheel) {
1248            if (velocityY > 0) {
1249                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1250            } else {
1251                flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1252            }
1253        } else {
1254            if (velocityY > 0) {
1255                int maxY = mTextSize * (mValue - mMinValue);
1256                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY);
1257            } else {
1258                int startY = mTextSize * (mMaxValue - mValue);
1259                int maxY = startY;
1260                flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY);
1261            }
1262        }
1263
1264        postAdjustScrollerCommand(flingScroller.getDuration());
1265        invalidate();
1266    }
1267
1268    /**
1269     * Hides the input controls which is the up/down arrows and the text field.
1270     */
1271    private void hideInputControls() {
1272        mShowInputControlsAnimator.cancel();
1273        mIncrementButton.setVisibility(INVISIBLE);
1274        mDecrementButton.setVisibility(INVISIBLE);
1275        mInputText.setVisibility(INVISIBLE);
1276    }
1277
1278    /**
1279     * Show the input controls by making them visible and animating the alpha
1280     * property up/down arrows.
1281     *
1282     * @param animationDuration The duration of the animation.
1283     */
1284    private void showInputControls(long animationDuration) {
1285        updateIncrementAndDecrementButtonsVisibilityState();
1286        mInputText.setVisibility(VISIBLE);
1287        mShowInputControlsAnimator.setDuration(animationDuration);
1288        mShowInputControlsAnimator.start();
1289    }
1290
1291    /**
1292     * Updates the visibility state of the increment and decrement buttons.
1293     */
1294    private void updateIncrementAndDecrementButtonsVisibilityState() {
1295        if (mWrapSelectorWheel || mValue < mMaxValue) {
1296            mIncrementButton.setVisibility(VISIBLE);
1297        } else {
1298            mIncrementButton.setVisibility(INVISIBLE);
1299        }
1300        if (mWrapSelectorWheel || mValue > mMinValue) {
1301            mDecrementButton.setVisibility(VISIBLE);
1302        } else {
1303            mDecrementButton.setVisibility(INVISIBLE);
1304        }
1305    }
1306
1307    /**
1308     * @return The selector indices array with proper values with the current as
1309     *         the middle one.
1310     */
1311    private int[] getSelectorIndices() {
1312        int current = getValue();
1313        if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) {
1314            for (int i = 0; i < mSelectorIndices.length; i++) {
1315                int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1316                if (mWrapSelectorWheel) {
1317                    selectorIndex = getWrappedSelectorIndex(selectorIndex);
1318                }
1319                mSelectorIndices[i] = selectorIndex;
1320                ensureCachedScrollSelectorValue(mSelectorIndices[i]);
1321            }
1322        }
1323        return mSelectorIndices;
1324    }
1325
1326    /**
1327     * @return The wrapped index <code>selectorIndex</code> value.
1328     */
1329    private int getWrappedSelectorIndex(int selectorIndex) {
1330        if (selectorIndex > mMaxValue) {
1331            return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
1332        } else if (selectorIndex < mMinValue) {
1333            return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
1334        }
1335        return selectorIndex;
1336    }
1337
1338    /**
1339     * Increments the <code>selectorIndices</code> whose string representations
1340     * will be displayed in the selector.
1341     */
1342    private void incrementScrollSelectorIndices(int[] selectorIndices) {
1343        for (int i = 0; i < selectorIndices.length - 1; i++) {
1344            selectorIndices[i] = selectorIndices[i + 1];
1345        }
1346        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1347        if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
1348            nextScrollSelectorIndex = mMinValue;
1349        }
1350        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1351        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1352    }
1353
1354    /**
1355     * Decrements the <code>selectorIndices</code> whose string representations
1356     * will be displayed in the selector.
1357     */
1358    private void decrementSelectorIndices(int[] selectorIndices) {
1359        for (int i = selectorIndices.length - 1; i > 0; i--) {
1360            selectorIndices[i] = selectorIndices[i - 1];
1361        }
1362        int nextScrollSelectorIndex = selectorIndices[1] - 1;
1363        if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
1364            nextScrollSelectorIndex = mMaxValue;
1365        }
1366        selectorIndices[0] = nextScrollSelectorIndex;
1367        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1368    }
1369
1370    /**
1371     * Ensures we have a cached string representation of the given <code>
1372     * selectorIndex</code>
1373     * to avoid multiple instantiations of the same string.
1374     */
1375    private void ensureCachedScrollSelectorValue(int selectorIndex) {
1376        SparseArray<String> cache = mSelectorIndexToStringCache;
1377        String scrollSelectorValue = cache.get(selectorIndex);
1378        if (scrollSelectorValue != null) {
1379            return;
1380        }
1381        if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
1382            scrollSelectorValue = "";
1383        } else {
1384            if (mDisplayedValues != null) {
1385                int displayedValueIndex = selectorIndex - mMinValue;
1386                scrollSelectorValue = mDisplayedValues[displayedValueIndex];
1387            } else {
1388                scrollSelectorValue = formatNumber(selectorIndex);
1389            }
1390        }
1391        cache.put(selectorIndex, scrollSelectorValue);
1392    }
1393
1394    private String formatNumber(int value) {
1395        return (mFormatter != null) ? mFormatter.format(value) : String.valueOf(value);
1396    }
1397
1398    private void validateInputTextView(View v) {
1399        String str = String.valueOf(((TextView) v).getText());
1400        if (TextUtils.isEmpty(str)) {
1401            // Restore to the old value as we don't allow empty values
1402            updateInputTextView();
1403        } else {
1404            // Check the new value and ensure it's in range
1405            int current = getSelectedPos(str.toString());
1406            changeCurrent(current);
1407        }
1408    }
1409
1410    /**
1411     * Updates the view of this NumberPicker. If displayValues were specified in
1412     * the string corresponding to the index specified by the current value will
1413     * be returned. Otherwise, the formatter specified in {@link #setFormatter}
1414     * will be used to format the number.
1415     */
1416    private void updateInputTextView() {
1417        /*
1418         * If we don't have displayed values then use the current number else
1419         * find the correct value in the displayed values for the current
1420         * number.
1421         */
1422        if (mDisplayedValues == null) {
1423            mInputText.setText(formatNumber(mValue));
1424        } else {
1425            mInputText.setText(mDisplayedValues[mValue - mMinValue]);
1426        }
1427        mInputText.setSelection(mInputText.getText().length());
1428    }
1429
1430    /**
1431     * Notifies the listener, if registered, of a change of the value of this
1432     * NumberPicker.
1433     */
1434    private void notifyChange(int previous, int current) {
1435        if (mOnValueChangedListener != null) {
1436            mOnValueChangedListener.onValueChange(this, previous, mValue);
1437        }
1438    }
1439
1440    /**
1441     * Posts a command for updating the current value every <code>updateMillis
1442     * </code>.
1443     */
1444    private void postUpdateValueFromLongPress(int updateMillis) {
1445        mInputText.clearFocus();
1446        removeAllCallbacks();
1447        if (mUpdateFromLongPressCommand == null) {
1448            mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand();
1449        }
1450        mUpdateFromLongPressCommand.setUpdateStep(updateMillis);
1451        post(mUpdateFromLongPressCommand);
1452    }
1453
1454    /**
1455     * Removes all pending callback from the message queue.
1456     */
1457    private void removeAllCallbacks() {
1458        if (mUpdateFromLongPressCommand != null) {
1459            removeCallbacks(mUpdateFromLongPressCommand);
1460        }
1461        if (mAdjustScrollerCommand != null) {
1462            removeCallbacks(mAdjustScrollerCommand);
1463        }
1464        if (mSetSelectionCommand != null) {
1465            removeCallbacks(mSetSelectionCommand);
1466        }
1467    }
1468
1469    /**
1470     * @return The selected index given its displayed <code>value</code>.
1471     */
1472    private int getSelectedPos(String value) {
1473        if (mDisplayedValues == null) {
1474            try {
1475                return Integer.parseInt(value);
1476            } catch (NumberFormatException e) {
1477                // Ignore as if it's not a number we don't care
1478            }
1479        } else {
1480            for (int i = 0; i < mDisplayedValues.length; i++) {
1481                // Don't force the user to type in jan when ja will do
1482                value = value.toLowerCase();
1483                if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1484                    return mMinValue + i;
1485                }
1486            }
1487
1488            /*
1489             * The user might have typed in a number into the month field i.e.
1490             * 10 instead of OCT so support that too.
1491             */
1492            try {
1493                return Integer.parseInt(value);
1494            } catch (NumberFormatException e) {
1495
1496                // Ignore as if it's not a number we don't care
1497            }
1498        }
1499        return mMinValue;
1500    }
1501
1502    /**
1503     * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1504     * </code> to
1505     * <code>selectionEnd</code>.
1506     */
1507    private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1508        if (mSetSelectionCommand == null) {
1509            mSetSelectionCommand = new SetSelectionCommand();
1510        } else {
1511            removeCallbacks(mSetSelectionCommand);
1512        }
1513        mSetSelectionCommand.mSelectionStart = selectionStart;
1514        mSetSelectionCommand.mSelectionEnd = selectionEnd;
1515        post(mSetSelectionCommand);
1516    }
1517
1518    /**
1519     * Posts an {@link AdjustScrollerCommand} within the given <code>
1520     * delayMillis</code>
1521     * .
1522     */
1523    private void postAdjustScrollerCommand(int delayMillis) {
1524        if (mAdjustScrollerCommand == null) {
1525            mAdjustScrollerCommand = new AdjustScrollerCommand();
1526        } else {
1527            removeCallbacks(mAdjustScrollerCommand);
1528        }
1529        postDelayed(mAdjustScrollerCommand, delayMillis);
1530    }
1531
1532    /**
1533     * Filter for accepting only valid indices or prefixes of the string
1534     * representation of valid indices.
1535     */
1536    class InputTextFilter extends NumberKeyListener {
1537
1538        // XXX This doesn't allow for range limits when controlled by a
1539        // soft input method!
1540        public int getInputType() {
1541            return InputType.TYPE_CLASS_TEXT;
1542        }
1543
1544        @Override
1545        protected char[] getAcceptedChars() {
1546            return DIGIT_CHARACTERS;
1547        }
1548
1549        @Override
1550        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
1551                int dstart, int dend) {
1552            if (mDisplayedValues == null) {
1553                CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1554                if (filtered == null) {
1555                    filtered = source.subSequence(start, end);
1556                }
1557
1558                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1559                        + dest.subSequence(dend, dest.length());
1560
1561                if ("".equals(result)) {
1562                    return result;
1563                }
1564                int val = getSelectedPos(result);
1565
1566                /*
1567                 * Ensure the user can't type in a value greater than the max
1568                 * allowed. We have to allow less than min as the user might
1569                 * want to delete some numbers and then type a new number.
1570                 */
1571                if (val > mMaxValue) {
1572                    return "";
1573                } else {
1574                    return filtered;
1575                }
1576            } else {
1577                CharSequence filtered = String.valueOf(source.subSequence(start, end));
1578                if (TextUtils.isEmpty(filtered)) {
1579                    return "";
1580                }
1581                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1582                        + dest.subSequence(dend, dest.length());
1583                String str = String.valueOf(result).toLowerCase();
1584                for (String val : mDisplayedValues) {
1585                    String valLowerCase = val.toLowerCase();
1586                    if (valLowerCase.startsWith(str)) {
1587                        postSetSelectionCommand(result.length(), val.length());
1588                        return val.subSequence(dstart, val.length());
1589                    }
1590                }
1591                return "";
1592            }
1593        }
1594    }
1595
1596    /**
1597     * Command for setting the input text selection.
1598     */
1599    class SetSelectionCommand implements Runnable {
1600        private int mSelectionStart;
1601
1602        private int mSelectionEnd;
1603
1604        public void run() {
1605            mInputText.setSelection(mSelectionStart, mSelectionEnd);
1606        }
1607    }
1608
1609    /**
1610     * Command for adjusting the scroller to show in its center the closest of
1611     * the displayed items.
1612     */
1613    class AdjustScrollerCommand implements Runnable {
1614        public void run() {
1615            mPreviousScrollerY = 0;
1616            if (mInitialScrollOffset == mCurrentScrollOffset) {
1617                updateInputTextView();
1618                showInputControls(mShowInputControlsAnimimationDuration);
1619                return;
1620            }
1621            // adjust to the closest value
1622            int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
1623            if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
1624                deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
1625            }
1626            mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
1627            invalidate();
1628        }
1629    }
1630
1631    /**
1632     * Command for updating the current value from a long press.
1633     */
1634    class UpdateValueFromLongPressCommand implements Runnable {
1635        private int mUpdateStep = 0;
1636
1637        private void setUpdateStep(int updateStep) {
1638            mUpdateStep = updateStep;
1639        }
1640
1641        public void run() {
1642            changeCurrent(mValue + mUpdateStep);
1643            postDelayed(this, mLongPressUpdateInterval);
1644        }
1645    }
1646}
1647