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