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