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