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