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