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