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