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