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