NumberPicker.java revision 01fa0d7aae1a551e1e7cfb90d2aeaf2fcb3978af
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 showInputText = ObjectAnimator.ofFloat(mInputText,
553                "alpha", 0, 1);
554        final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton,
555                "alpha", 0, 1);
556        mShowInputControlsAnimator = new AnimatorSet();
557        mShowInputControlsAnimator.playTogether(fadeScroller, showIncrementButton,
558                showInputText, showDecrementButton);
559        mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() {
560            private boolean mCanceled = false;
561
562            @Override
563            public void onAnimationEnd(Animator animation) {
564                if (!mCanceled) {
565                    // if canceled => we still want the wheel drawn
566                    setDrawSelectorWheel(false);
567                }
568                mCanceled = false;
569                mSelectorPaint.setAlpha(255);
570                invalidate();
571            }
572
573            @Override
574            public void onAnimationCancel(Animator animation) {
575                if (mShowInputControlsAnimator.isRunning()) {
576                    mCanceled = true;
577                }
578            }
579        });
580
581        // create the fling and adjust scrollers
582        mFlingScroller = new Scroller(getContext(), null, true);
583        mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
584
585        updateInputTextView();
586        updateIncrementAndDecrementButtonsVisibilityState();
587
588        if (mFlingable && !isInEditMode()) {
589            // Start with shown selector wheel and hidden controls. When made
590            // visible hide the selector and fade-in the controls to suggest
591            // fling interaction.
592            setDrawSelectorWheel(true);
593            hideInputControls();
594        }
595    }
596
597    @Override
598    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
599        super.onLayout(changed, left, top, right, bottom);
600        // need to do this when we know our size
601        initializeScrollWheel();
602    }
603
604    @Override
605    public boolean onInterceptTouchEvent(MotionEvent event) {
606        if (!isEnabled() || !mFlingable) {
607            return false;
608        }
609        switch (event.getActionMasked()) {
610            case MotionEvent.ACTION_DOWN:
611                mLastMotionEventY = mLastDownEventY = event.getY();
612                removeAllCallbacks();
613                mShowInputControlsAnimator.cancel();
614                mBeginEditOnUpEvent = false;
615                mAdjustScrollerOnUpEvent = true;
616                if (mDrawSelectorWheel) {
617                    boolean scrollersFinished = mFlingScroller.isFinished()
618                            && mAdjustScroller.isFinished();
619                    if (!scrollersFinished) {
620                        mFlingScroller.forceFinished(true);
621                        mAdjustScroller.forceFinished(true);
622                        onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
623                    }
624                    mBeginEditOnUpEvent = scrollersFinished;
625                    mAdjustScrollerOnUpEvent = true;
626                    hideInputControls();
627                    return true;
628                }
629                if (isEventInViewHitRect(event, mInputText)
630                        || (!mIncrementButton.isShown()
631                                && isEventInViewHitRect(event, mIncrementButton))
632                        || (!mDecrementButton.isShown()
633                                && isEventInViewHitRect(event, mDecrementButton))) {
634                    mAdjustScrollerOnUpEvent = false;
635                    setDrawSelectorWheel(true);
636                    hideInputControls();
637                    return true;
638                }
639                break;
640            case MotionEvent.ACTION_MOVE:
641                float currentMoveY = event.getY();
642                int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
643                if (deltaDownY > mTouchSlop) {
644                    mBeginEditOnUpEvent = false;
645                    onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
646                    setDrawSelectorWheel(true);
647                    hideInputControls();
648                    return true;
649                }
650                break;
651        }
652        return false;
653    }
654
655    @Override
656    public boolean onTouchEvent(MotionEvent ev) {
657        if (!isEnabled()) {
658            return false;
659        }
660        if (mVelocityTracker == null) {
661            mVelocityTracker = VelocityTracker.obtain();
662        }
663        mVelocityTracker.addMovement(ev);
664        int action = ev.getActionMasked();
665        switch (action) {
666            case MotionEvent.ACTION_MOVE:
667                float currentMoveY = ev.getY();
668                if (mBeginEditOnUpEvent
669                        || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
670                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
671                    if (deltaDownY > mTouchSlop) {
672                        mBeginEditOnUpEvent = false;
673                        onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
674                    }
675                }
676                int deltaMoveY = (int) (currentMoveY - mLastMotionEventY);
677                scrollBy(0, deltaMoveY);
678                invalidate();
679                mLastMotionEventY = currentMoveY;
680                break;
681            case MotionEvent.ACTION_UP:
682                if (mBeginEditOnUpEvent) {
683                    setDrawSelectorWheel(false);
684                    showInputControls(mShowInputControlsAnimimationDuration);
685                    mInputText.requestFocus();
686                    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
687                            Context.INPUT_METHOD_SERVICE);
688                    imm.showSoftInput(mInputText, 0);
689                    mInputText.setSelection(0, mInputText.getText().length());
690                    return true;
691                }
692                VelocityTracker velocityTracker = mVelocityTracker;
693                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
694                int initialVelocity = (int) velocityTracker.getYVelocity();
695                if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
696                    fling(initialVelocity);
697                    onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
698                } else {
699                    if (mAdjustScrollerOnUpEvent) {
700                        if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) {
701                            postAdjustScrollerCommand(0);
702                        }
703                    } else {
704                        postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS);
705                    }
706                }
707                mVelocityTracker.recycle();
708                mVelocityTracker = null;
709                break;
710        }
711        return true;
712    }
713
714    @Override
715    public boolean dispatchTouchEvent(MotionEvent event) {
716        int action = event.getActionMasked();
717        if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)
718                && !isEventInViewHitRect(event, mInputText)) {
719            removeAllCallbacks();
720        }
721        return super.dispatchTouchEvent(event);
722    }
723
724    @Override
725    public boolean dispatchKeyEvent(KeyEvent event) {
726        int keyCode = event.getKeyCode();
727        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
728            removeAllCallbacks();
729        }
730        return super.dispatchKeyEvent(event);
731    }
732
733    @Override
734    public boolean dispatchTrackballEvent(MotionEvent event) {
735        int action = event.getActionMasked();
736        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
737            removeAllCallbacks();
738        }
739        return super.dispatchTrackballEvent(event);
740    }
741
742    @Override
743    public void computeScroll() {
744        if (!mDrawSelectorWheel) {
745            return;
746        }
747        Scroller scroller = mFlingScroller;
748        if (scroller.isFinished()) {
749            scroller = mAdjustScroller;
750            if (scroller.isFinished()) {
751                return;
752            }
753        }
754        scroller.computeScrollOffset();
755        int currentScrollerY = scroller.getCurrY();
756        if (mPreviousScrollerY == 0) {
757            mPreviousScrollerY = scroller.getStartY();
758        }
759        scrollBy(0, currentScrollerY - mPreviousScrollerY);
760        mPreviousScrollerY = currentScrollerY;
761        if (scroller.isFinished()) {
762            onScrollerFinished(scroller);
763        } else {
764            invalidate();
765        }
766    }
767
768    @Override
769    public void setEnabled(boolean enabled) {
770        super.setEnabled(enabled);
771        mIncrementButton.setEnabled(enabled);
772        mDecrementButton.setEnabled(enabled);
773        mInputText.setEnabled(enabled);
774    }
775
776    @Override
777    public void scrollBy(int x, int y) {
778        int[] selectorIndices = getSelectorIndices();
779        if (!mWrapSelectorWheel && y > 0
780                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
781            mCurrentScrollOffset = mInitialScrollOffset;
782            return;
783        }
784        if (!mWrapSelectorWheel && y < 0
785                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
786            mCurrentScrollOffset = mInitialScrollOffset;
787            return;
788        }
789        mCurrentScrollOffset += y;
790        while (mCurrentScrollOffset - mInitialScrollOffset >= mSelectorElementHeight) {
791            mCurrentScrollOffset -= mSelectorElementHeight;
792            decrementSelectorIndices(selectorIndices);
793            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
794            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
795                mCurrentScrollOffset = mInitialScrollOffset;
796            }
797        }
798        while (mCurrentScrollOffset - mInitialScrollOffset <= -mSelectorElementHeight) {
799            mCurrentScrollOffset += mSelectorElementHeight;
800            incrementScrollSelectorIndices(selectorIndices);
801            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
802            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
803                mCurrentScrollOffset = mInitialScrollOffset;
804            }
805        }
806    }
807
808    @Override
809    public int getSolidColor() {
810        return mSolidColor;
811    }
812
813    /**
814     * Sets the listener to be notified on change of the current value.
815     *
816     * @param onValueChangedListener The listener.
817     */
818    public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
819        mOnValueChangeListener = onValueChangedListener;
820    }
821
822    /**
823     * Set listener to be notified for scroll state changes.
824     *
825     * @param onScrollListener The listener.
826     */
827    public void setOnScrollListener(OnScrollListener onScrollListener) {
828        mOnScrollListener = onScrollListener;
829    }
830
831    /**
832     * Set the formatter to be used for formatting the current value.
833     * <p>
834     * Note: If you have provided alternative values for the values this
835     * formatter is never invoked.
836     * </p>
837     *
838     * @param formatter The formatter object. If formatter is <code>null</code>,
839     *            {@link String#valueOf(int)} will be used.
840     *
841     * @see #setDisplayedValues(String[])
842     */
843    public void setFormatter(Formatter formatter) {
844        if (formatter == mFormatter) {
845            return;
846        }
847        mFormatter = formatter;
848        resetSelectorWheelIndices();
849        updateInputTextView();
850    }
851
852    /**
853     * Set the current value for the number picker.
854     * <p>
855     * If the argument is less than the {@link NumberPicker#getMinValue()} and
856     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
857     * current value is set to the {@link NumberPicker#getMinValue()} value.
858     * </p>
859     * <p>
860     * If the argument is less than the {@link NumberPicker#getMinValue()} and
861     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</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>false</code> the
867     * current value is set to the {@link NumberPicker#getMaxValue()} value.
868     * </p>
869     * <p>
870     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
871     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
872     * current value is set to the {@link NumberPicker#getMinValue()} value.
873     * </p>
874     *
875     * @param value The current value.
876     * @see #setWrapSelectorWheel(boolean)
877     * @see #setMinValue(int)
878     * @see #setMaxValue(int)
879     */
880    public void setValue(int value) {
881        if (mValue == value) {
882            return;
883        }
884        if (value < mMinValue) {
885            value = mWrapSelectorWheel ? mMaxValue : mMinValue;
886        }
887        if (value > mMaxValue) {
888            value = mWrapSelectorWheel ? mMinValue : mMaxValue;
889        }
890        mValue = value;
891        updateInputTextView();
892        updateIncrementAndDecrementButtonsVisibilityState();
893    }
894
895    /**
896     * Gets whether the selector wheel wraps when reaching the min/max value.
897     *
898     * @return True if the selector wheel wraps.
899     *
900     * @see #getMinValue()
901     * @see #getMaxValue()
902     */
903    public boolean getWrapSelectorWheel() {
904        return mWrapSelectorWheel;
905    }
906
907    /**
908     * Sets whether the selector wheel shown during flinging/scrolling should
909     * wrap around the {@link NumberPicker#getMinValue()} and
910     * {@link NumberPicker#getMaxValue()} values.
911     * <p>
912     * By default if the range (max - min) is more than five (the number of
913     * items shown on the selector wheel) the selector wheel wrapping is
914     * enabled.
915     * </p>
916     *
917     * @param wrapSelector Whether to wrap.
918     */
919    public void setWrapSelectorWheel(boolean wrapSelector) {
920        if (wrapSelector && (mMaxValue - mMinValue) < mSelectorIndices.length) {
921            throw new IllegalStateException("Range less than selector items count.");
922        }
923        if (wrapSelector != mWrapSelectorWheel) {
924            // force the selector indices array to be reinitialized
925            mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE;
926            mWrapSelectorWheel = wrapSelector;
927            // force redraw since we might look different
928            updateIncrementAndDecrementButtonsVisibilityState();
929        }
930    }
931
932    /**
933     * Sets the speed at which the numbers be incremented and decremented when
934     * the up and down buttons are long pressed respectively.
935     * <p>
936     * The default value is 300 ms.
937     * </p>
938     *
939     * @param intervalMillis The speed (in milliseconds) at which the numbers
940     *            will be incremented and decremented.
941     */
942    public void setOnLongPressUpdateInterval(long intervalMillis) {
943        mLongPressUpdateInterval = intervalMillis;
944    }
945
946    /**
947     * Returns the value of the picker.
948     *
949     * @return The value.
950     */
951    public int getValue() {
952        return mValue;
953    }
954
955    /**
956     * Returns the min value of the picker.
957     *
958     * @return The min value
959     */
960    public int getMinValue() {
961        return mMinValue;
962    }
963
964    /**
965     * Sets the min value of the picker.
966     *
967     * @param minValue The min value.
968     */
969    public void setMinValue(int minValue) {
970        if (mMinValue == minValue) {
971            return;
972        }
973        if (minValue < 0) {
974            throw new IllegalArgumentException("minValue must be >= 0");
975        }
976        mMinValue = minValue;
977        if (mMinValue > mValue) {
978            mValue = mMinValue;
979        }
980        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
981        setWrapSelectorWheel(wrapSelectorWheel);
982        resetSelectorWheelIndices();
983        updateInputTextView();
984    }
985
986    /**
987     * Returns the max value of the picker.
988     *
989     * @return The max value.
990     */
991    public int getMaxValue() {
992        return mMaxValue;
993    }
994
995    /**
996     * Sets the max value of the picker.
997     *
998     * @param maxValue The max value.
999     */
1000    public void setMaxValue(int maxValue) {
1001        if (mMaxValue == maxValue) {
1002            return;
1003        }
1004        if (maxValue < 0) {
1005            throw new IllegalArgumentException("maxValue must be >= 0");
1006        }
1007        mMaxValue = maxValue;
1008        if (mMaxValue < mValue) {
1009            mValue = mMaxValue;
1010        }
1011        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1012        setWrapSelectorWheel(wrapSelectorWheel);
1013        resetSelectorWheelIndices();
1014        updateInputTextView();
1015    }
1016
1017    /**
1018     * Gets the values to be displayed instead of string values.
1019     *
1020     * @return The displayed values.
1021     */
1022    public String[] getDisplayedValues() {
1023        return mDisplayedValues;
1024    }
1025
1026    /**
1027     * Sets the values to be displayed.
1028     *
1029     * @param displayedValues The displayed values.
1030     */
1031    public void setDisplayedValues(String[] displayedValues) {
1032        if (mDisplayedValues == displayedValues) {
1033            return;
1034        }
1035        mDisplayedValues = displayedValues;
1036        if (mDisplayedValues != null) {
1037            // Allow text entry rather than strictly numeric entry.
1038            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
1039                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
1040        } else {
1041            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1042        }
1043        updateInputTextView();
1044        resetSelectorWheelIndices();
1045    }
1046
1047    @Override
1048    protected float getTopFadingEdgeStrength() {
1049        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1050    }
1051
1052    @Override
1053    protected float getBottomFadingEdgeStrength() {
1054        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1055    }
1056
1057    @Override
1058    protected void onAttachedToWindow() {
1059        super.onAttachedToWindow();
1060        // make sure we show the controls only the very
1061        // first time the user sees this widget
1062        if (mFlingable && !isInEditMode()) {
1063            // animate a bit slower the very first time
1064            showInputControls(mShowInputControlsAnimimationDuration * 2);
1065        }
1066    }
1067
1068    @Override
1069    protected void onDetachedFromWindow() {
1070        removeAllCallbacks();
1071    }
1072
1073    @Override
1074    protected void dispatchDraw(Canvas canvas) {
1075        // There is a good reason for doing this. See comments in draw().
1076    }
1077
1078    @Override
1079    public void draw(Canvas canvas) {
1080        // Dispatch draw to our children only if we are not currently running
1081        // the animation for simultaneously fading out the scroll wheel and
1082        // showing in the buttons. This class takes advantage of the View
1083        // implementation of fading edges effect to draw the selector wheel.
1084        // However, in View.draw(), the fading is applied after all the children
1085        // have been drawn and we do not want this fading to be applied to the
1086        // buttons which are currently showing in. Therefore, we draw our
1087        // children after we have completed drawing ourselves.
1088        super.draw(canvas);
1089
1090        // Draw our children if we are not showing the selector wheel of fading
1091        // it out
1092        if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) {
1093            long drawTime = getDrawingTime();
1094            for (int i = 0, count = getChildCount(); i < count; i++) {
1095                View child = getChildAt(i);
1096                if (!child.isShown()) {
1097                    continue;
1098                }
1099                drawChild(canvas, getChildAt(i), drawTime);
1100            }
1101        }
1102    }
1103
1104    @Override
1105    protected void onDraw(Canvas canvas) {
1106        // we only draw the selector wheel
1107        if (!mDrawSelectorWheel) {
1108            return;
1109        }
1110        float x = (mRight - mLeft) / 2;
1111        float y = mCurrentScrollOffset;
1112
1113        // draw the selector wheel
1114        int[] selectorIndices = getSelectorIndices();
1115        for (int i = 0; i < selectorIndices.length; i++) {
1116            int selectorIndex = selectorIndices[i];
1117            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1118            canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint);
1119            y += mSelectorElementHeight;
1120        }
1121
1122        // draw the selection dividers (only if scrolling and drawable specified)
1123        if (mSelectionDivider != null) {
1124            mSelectionDivider.setAlpha(mSelectorPaint.getAlpha());
1125            // draw the top divider
1126            int topOfTopDivider =
1127                (getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2;
1128            int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
1129            mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
1130            mSelectionDivider.draw(canvas);
1131
1132            // draw the bottom divider
1133            int topOfBottomDivider =  topOfTopDivider + mSelectorElementHeight;
1134            int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight;
1135            mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
1136            mSelectionDivider.draw(canvas);
1137        }
1138    }
1139
1140    /**
1141     * Resets the selector indices and clear the cached
1142     * string representation of these indices.
1143     */
1144    private void resetSelectorWheelIndices() {
1145        mSelectorIndexToStringCache.clear();
1146        int[] selectorIdices = getSelectorIndices();
1147        for (int i = 0; i < selectorIdices.length; i++) {
1148            selectorIdices[i] = Integer.MIN_VALUE;
1149        }
1150    }
1151
1152    /**
1153     * Sets the current value of this NumberPicker, and sets mPrevious to the
1154     * previous value. If current is greater than mEnd less than mStart, the
1155     * value of mCurrent is wrapped around. Subclasses can override this to
1156     * change the wrapping behavior
1157     *
1158     * @param current the new value of the NumberPicker
1159     */
1160    private void changeCurrent(int current) {
1161        if (mValue == current) {
1162            return;
1163        }
1164        // Wrap around the values if we go past the start or end
1165        if (mWrapSelectorWheel) {
1166            current = getWrappedSelectorIndex(current);
1167        }
1168        int previous = mValue;
1169        setValue(current);
1170        notifyChange(previous, current);
1171    }
1172
1173    /**
1174     * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
1175     * wheel.
1176     */
1177    @SuppressWarnings("unused")
1178    // Called by ShowInputControlsAnimator via reflection
1179    private void setSelectorPaintAlpha(int alpha) {
1180        mSelectorPaint.setAlpha(alpha);
1181        if (mDrawSelectorWheel) {
1182            invalidate();
1183        }
1184    }
1185
1186    /**
1187     * @return If the <code>event</code> is in the <code>view</code>.
1188     */
1189    private boolean isEventInViewHitRect(MotionEvent event, View view) {
1190        view.getHitRect(mTempRect);
1191        return mTempRect.contains((int) event.getX(), (int) event.getY());
1192    }
1193
1194    /**
1195     * Sets if to <code>drawSelectionWheel</code>.
1196     */
1197    private void setDrawSelectorWheel(boolean drawSelectorWheel) {
1198        mDrawSelectorWheel = drawSelectorWheel;
1199        // do not fade if the selector wheel not shown
1200        setVerticalFadingEdgeEnabled(drawSelectorWheel);
1201    }
1202
1203    private void initializeScrollWheel() {
1204        if (mInitialScrollOffset != Integer.MIN_VALUE) {
1205            return;
1206
1207        }
1208        int[] selectorIndices = getSelectorIndices();
1209        int totalTextHeight = selectorIndices.length * mTextSize;
1210        float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
1211        float textGapCount = selectorIndices.length - 1;
1212        int selectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
1213        // Compensate if text size is odd since every time we get its middle a pixel is lost.
1214        mInitialScrollOffset = mCurrentScrollOffset = mTextSize - (3 * (mTextSize % 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