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