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