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