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