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