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