NumberPicker.java revision f0ef665299d795df7905897e1c337e37891dafef
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.text.InputFilter;
36import android.text.InputType;
37import android.text.Spanned;
38import android.text.TextUtils;
39import android.text.method.NumberKeyListener;
40import android.util.AttributeSet;
41import android.util.SparseArray;
42import android.view.KeyEvent;
43import android.view.LayoutInflater;
44import android.view.MotionEvent;
45import android.view.VelocityTracker;
46import android.view.View;
47import android.view.ViewConfiguration;
48import android.view.LayoutInflater.Filter;
49import android.view.animation.OvershootInterpolator;
50import android.view.inputmethod.InputMethodManager;
51
52/**
53 * A view for selecting a number For a dialog using this view, see
54 * {@link android.app.TimePickerDialog}.
55 *
56 * @hide
57 */
58@Widget
59public class NumberPicker extends LinearLayout {
60
61    /**
62     * The index of the middle selector item.
63     */
64    private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2;
65
66    /**
67     * The coefficient by which to adjust (divide) the max fling velocity.
68     */
69    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
70
71    /**
72     * The the duration for adjusting the selector wheel.
73     */
74    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
75
76    /**
77     * The the delay for showing the input controls after a single tap on the
78     * input text.
79     */
80    private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration
81            .getDoubleTapTimeout();
82
83    /**
84     * The update step for incrementing the current value.
85     */
86    private static final int UPDATE_STEP_INCREMENT = 1;
87
88    /**
89     * The update step for decrementing the current value.
90     */
91    private static final int UPDATE_STEP_DECREMENT = -1;
92
93    /**
94     * The strength of fading in the top and bottom while drawing the selector.
95     */
96    private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
97
98    /**
99     * The numbers accepted by the input text's {@link Filter}
100     */
101    private static final char[] DIGIT_CHARACTERS = new char[] {
102            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
103    };
104
105    /**
106     * Use a custom NumberPicker formatting callback to use two-digit minutes
107     * strings like "01". Keeping a static formatter etc. is the most efficient
108     * way to do this; it avoids creating temporary objects on every call to
109     * format().
110     */
111    public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
112        final StringBuilder mBuilder = new StringBuilder();
113
114        final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
115
116        final Object[] mArgs = new Object[1];
117
118        public String toString(int value) {
119            mArgs[0] = value;
120            mBuilder.delete(0, mBuilder.length());
121            mFmt.format("%02d", mArgs);
122            return mFmt.toString();
123        }
124    };
125
126    /**
127     * The increment button.
128     */
129    private final ImageButton mIncrementButton;
130
131    /**
132     * The decrement button.
133     */
134    private final ImageButton mDecrementButton;
135
136    /**
137     * The text for showing the current value.
138     */
139    private final EditText mInputText;
140
141    /**
142     * The height of the text.
143     */
144    private final int mTextSize;
145
146    /**
147     * The values to be displayed instead the indices.
148     */
149    private String[] mDisplayedValues;
150
151    /**
152     * Lower value of the range of numbers allowed for the NumberPicker
153     */
154    private int mStart;
155
156    /**
157     * Upper value of the range of numbers allowed for the NumberPicker
158     */
159    private int mEnd;
160
161    /**
162     * Current value of this NumberPicker
163     */
164    private int mCurrent;
165
166    /**
167     * Listener to be notified upon current value change.
168     */
169    private OnChangedListener mListener;
170
171    /**
172     * Formatter for for displaying the current value.
173     */
174    private Formatter mFormatter;
175
176    /**
177     * The speed for updating the value form long press.
178     */
179    private long mLongPressUpdateSpeed = 300;
180
181    /**
182     * Cache for the string representation of selector indices.
183     */
184    private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
185
186    /**
187     * The selector indices whose value are show by the selector.
188     */
189    private final int[] mSelectorIndices = new int[] {
190            Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE,
191            Integer.MIN_VALUE
192    };
193
194    /**
195     * The {@link Paint} for drawing the selector.
196     */
197    private final Paint mSelectorPaint;
198
199    /**
200     * The height of a selector element (text + gap).
201     */
202    private int mSelectorElementHeight;
203
204    /**
205     * The initial offset of the scroll selector.
206     */
207    private int mInitialScrollOffset = Integer.MIN_VALUE;
208
209    /**
210     * The current offset of the scroll selector.
211     */
212    private int mCurrentScrollOffset;
213
214    /**
215     * The {@link Scroller} responsible for flinging the selector.
216     */
217    private final Scroller mFlingScroller;
218
219    /**
220     * The {@link Scroller} responsible for adjusting the selector.
221     */
222    private final Scroller mAdjustScroller;
223
224    /**
225     * The previous Y coordinate while scrolling the selector.
226     */
227    private int mPreviousScrollerY;
228
229    /**
230     * Handle to the reusable command for setting the input text selection.
231     */
232    private SetSelectionCommand mSetSelectionCommand;
233
234    /**
235     * Handle to the reusable command for adjusting the scroller.
236     */
237    private AdjustScrollerCommand mAdjustScrollerCommand;
238
239    /**
240     * Handle to the reusable command for updating the current value from long
241     * press.
242     */
243    private UpdateValueFromLongPressCommand mUpdateFromLongPressCommand;
244
245    /**
246     * {@link Animator} for showing the up/down arrows.
247     */
248    private final AnimatorSet mShowInputControlsAnimator;
249
250    /**
251     * The Y position of the last down event.
252     */
253    private float mLastDownEventY;
254
255    /**
256     * The Y position of the last motion event.
257     */
258    private float mLastMotionEventY;
259
260    /**
261     * Flag if to begin edit on next up event.
262     */
263    private boolean mBeginEditOnUpEvent;
264
265    /**
266     * Flag if to adjust the selector wheel on next up event.
267     */
268    private boolean mAdjustScrollerOnUpEvent;
269
270    /**
271     * Flag if to draw the selector wheel.
272     */
273    private boolean mDrawSelectorWheel;
274
275    /**
276     * Determines speed during touch scrolling.
277     */
278    private VelocityTracker mVelocityTracker;
279
280    /**
281     * @see ViewConfiguration#getScaledTouchSlop()
282     */
283    private int mTouchSlop;
284
285    /**
286     * @see ViewConfiguration#getScaledMinimumFlingVelocity()
287     */
288    private int mMinimumFlingVelocity;
289
290    /**
291     * @see ViewConfiguration#getScaledMaximumFlingVelocity()
292     */
293    private int mMaximumFlingVelocity;
294
295    /**
296     * Flag whether the selector should wrap around.
297     */
298    private boolean mWrapSelector;
299
300    /**
301     * The back ground color used to optimize scroller fading.
302     */
303    private final int mSolidColor;
304
305    /**
306     * Reusable {@link Rect} instance.
307     */
308    private final Rect mTempRect = new Rect();
309
310    /**
311     * The callback interface used to indicate the number value has changed.
312     */
313    public interface OnChangedListener {
314        /**
315         * @param picker The NumberPicker associated with this listener.
316         * @param oldVal The previous value.
317         * @param newVal The new value.
318         */
319        void onChanged(NumberPicker picker, int oldVal, int newVal);
320    }
321
322    /**
323     * Interface used to format the number into a string for presentation
324     */
325    public interface Formatter {
326        String toString(int value);
327    }
328
329    /**
330     * Create a new number picker
331     *
332     * @param context The application environment.
333     * @param attrs A collection of attributes.
334     */
335    public NumberPicker(Context context, AttributeSet attrs) {
336        this(context, attrs, R.attr.numberPickerStyle);
337    }
338
339    /**
340     * Create a new number picker
341     *
342     * @param context the application environment.
343     * @param attrs a collection of attributes.
344     * @param defStyle The default style to apply to this view.
345     */
346    public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
347        super(context, attrs, defStyle);
348
349        // process style attributes
350        TypedArray attributesArray = context.obtainStyledAttributes(attrs,
351                R.styleable.NumberPicker, defStyle, 0);
352        int orientation = attributesArray.getInt(R.styleable.NumberPicker_orientation, VERTICAL);
353        setOrientation(orientation);
354        mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
355        attributesArray.recycle();
356
357        // By default Linearlayout that we extend is not drawn. This is
358        // its draw() method is not called but dispatchDraw() is called
359        // directly (see ViewGroup.drawChild()). However, this class uses
360        // the fading edge effect implemented by View and we need our
361        // draw() method to be called. Therefore, we declare we will draw.
362        setWillNotDraw(false);
363        setDrawSelectorWheel(false);
364
365        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
366                Context.LAYOUT_INFLATER_SERVICE);
367        inflater.inflate(R.layout.number_picker, this, true);
368
369        OnClickListener onClickListener = new OnClickListener() {
370            public void onClick(View v) {
371                mInputText.clearFocus();
372                if (v.getId() == R.id.increment) {
373                    changeCurrent(mCurrent + 1);
374                } else {
375                    changeCurrent(mCurrent - 1);
376                }
377            }
378        };
379
380        OnLongClickListener onLongClickListener = new OnLongClickListener() {
381            public boolean onLongClick(View v) {
382                mInputText.clearFocus();
383                if (v.getId() == R.id.increment) {
384                    postUpdateValueFromLongPress(UPDATE_STEP_INCREMENT);
385                } else {
386                    postUpdateValueFromLongPress(UPDATE_STEP_DECREMENT);
387                }
388                return true;
389            }
390        };
391
392        // increment button
393        mIncrementButton = (ImageButton) findViewById(R.id.increment);
394        mIncrementButton.setOnClickListener(onClickListener);
395        mIncrementButton.setOnLongClickListener(onLongClickListener);
396
397        // decrement button
398        mDecrementButton = (ImageButton) findViewById(R.id.decrement);
399        mDecrementButton.setOnClickListener(onClickListener);
400        mDecrementButton.setOnLongClickListener(onLongClickListener);
401
402        // input text
403        mInputText = (EditText) findViewById(R.id.timepicker_input);
404        mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
405            public void onFocusChange(View v, boolean hasFocus) {
406                if (!hasFocus) {
407                    validateInputTextView(v);
408                }
409            }
410        });
411        mInputText.setFilters(new InputFilter[] {
412            new InputTextFilter()
413        });
414
415        mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
416
417        // initialize constants
418        mTouchSlop = ViewConfiguration.getTapTimeout();
419        ViewConfiguration configuration = ViewConfiguration.get(context);
420        mTouchSlop = configuration.getScaledTouchSlop();
421        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
422        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
423                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
424        mTextSize = (int) mInputText.getTextSize();
425
426        // create the selector wheel paint
427        Paint paint = new Paint();
428        paint.setAntiAlias(true);
429        paint.setTextAlign(Align.CENTER);
430        paint.setTextSize(mTextSize);
431        paint.setTypeface(mInputText.getTypeface());
432        ColorStateList colors = mInputText.getTextColors();
433        int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
434        paint.setColor(color);
435        mSelectorPaint = paint;
436
437        // create the animator for showing the input controls
438        final ValueAnimator fadeScroller = ObjectAnimator.ofInt(this, "selectorPaintAlpha", 255, 0);
439        final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton,
440                "alpha", 0, 1);
441        final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton,
442                "alpha", 0, 1);
443        mShowInputControlsAnimator = new AnimatorSet();
444        mShowInputControlsAnimator.playTogether(fadeScroller, showIncrementButton,
445                showDecrementButton);
446        mShowInputControlsAnimator.setDuration(getResources().getInteger(
447                R.integer.config_longAnimTime));
448        mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() {
449            private boolean mCanceled = false;
450
451            @Override
452            public void onAnimationEnd(Animator animation) {
453                if (!mCanceled) {
454                    // if canceled => we still want the wheel drawn
455                    setDrawSelectorWheel(false);
456                }
457                mCanceled = false;
458                mSelectorPaint.setAlpha(255);
459                invalidate();
460            }
461
462            @Override
463            public void onAnimationCancel(Animator animation) {
464                if (mShowInputControlsAnimator.isRunning()) {
465                    mCanceled = true;
466                }
467            }
468        });
469
470        // create the fling and adjust scrollers
471        mFlingScroller = new Scroller(getContext());
472        mAdjustScroller = new Scroller(getContext(), new OvershootInterpolator());
473
474        updateInputTextView();
475        updateIncrementAndDecrementButtonsVisibilityState();
476    }
477
478    @Override
479    public void onWindowFocusChanged(boolean hasWindowFocus) {
480        super.onWindowFocusChanged(hasWindowFocus);
481        if (!hasWindowFocus) {
482            removeAllCallbacks();
483        }
484    }
485
486    @Override
487    public boolean onInterceptTouchEvent(MotionEvent event) {
488        switch (event.getActionMasked()) {
489            case MotionEvent.ACTION_DOWN:
490                mLastMotionEventY = mLastDownEventY = event.getY();
491                removeAllCallbacks();
492                mBeginEditOnUpEvent = false;
493                mAdjustScrollerOnUpEvent = true;
494                if (mDrawSelectorWheel) {
495                    mBeginEditOnUpEvent = mFlingScroller.isFinished()
496                            && mAdjustScroller.isFinished();
497                    mAdjustScrollerOnUpEvent = true;
498                    mFlingScroller.forceFinished(true);
499                    mAdjustScroller.forceFinished(true);
500                    hideInputControls();
501                    return true;
502                }
503                if (isEventInInputText(event)) {
504                    mAdjustScrollerOnUpEvent = false;
505                    setDrawSelectorWheel(true);
506                    hideInputControls();
507                    return true;
508                }
509                break;
510            case MotionEvent.ACTION_MOVE:
511                float currentMoveY = event.getY();
512                int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
513                if (deltaDownY > mTouchSlop) {
514                    mBeginEditOnUpEvent = false;
515                    setDrawSelectorWheel(true);
516                    hideInputControls();
517                    return true;
518                }
519                break;
520        }
521        return false;
522    }
523
524    @Override
525    public boolean onTouchEvent(MotionEvent ev) {
526        if (mVelocityTracker == null) {
527            mVelocityTracker = VelocityTracker.obtain();
528        }
529        mVelocityTracker.addMovement(ev);
530        int action = ev.getActionMasked();
531        switch (action) {
532            case MotionEvent.ACTION_MOVE:
533                float currentMoveY = ev.getY();
534                if (mBeginEditOnUpEvent) {
535                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
536                    if (deltaDownY > mTouchSlop) {
537                        mBeginEditOnUpEvent = false;
538                    }
539                }
540                int deltaMoveY = (int) (currentMoveY - mLastMotionEventY);
541                scrollBy(0, deltaMoveY);
542                invalidate();
543                mLastMotionEventY = currentMoveY;
544                break;
545            case MotionEvent.ACTION_UP:
546                if (mBeginEditOnUpEvent) {
547                    setDrawSelectorWheel(false);
548                    showInputControls();
549                    mInputText.requestFocus();
550                    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
551                            Context.INPUT_METHOD_SERVICE);
552                    imm.showSoftInput(mInputText, 0);
553                    return true;
554                }
555                VelocityTracker velocityTracker = mVelocityTracker;
556                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
557                int initialVelocity = (int) velocityTracker.getYVelocity();
558                if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
559                    fling(initialVelocity);
560                } else {
561                    if (mAdjustScrollerOnUpEvent) {
562                        if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) {
563                            postAdjustScrollerCommand(0);
564                        }
565                    } else {
566                        postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS);
567                    }
568                }
569                mVelocityTracker.recycle();
570                mVelocityTracker = null;
571                break;
572        }
573        return true;
574    }
575
576    @Override
577    public boolean dispatchTouchEvent(MotionEvent event) {
578        int action = event.getActionMasked();
579        if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)
580                && !isEventInInputText(event)) {
581            removeAllCallbacks();
582        }
583        return super.dispatchTouchEvent(event);
584    }
585
586    @Override
587    public boolean dispatchKeyEvent(KeyEvent event) {
588        int keyCode = event.getKeyCode();
589        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
590            removeAllCallbacks();
591        }
592        return super.dispatchKeyEvent(event);
593    }
594
595    @Override
596    public boolean dispatchTrackballEvent(MotionEvent event) {
597        int action = event.getActionMasked();
598        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
599            removeAllCallbacks();
600        }
601        return super.dispatchTrackballEvent(event);
602    }
603
604    @Override
605    public void computeScroll() {
606        if (!mDrawSelectorWheel) {
607            return;
608        }
609        Scroller scroller = mFlingScroller;
610        if (scroller.isFinished()) {
611            scroller = mAdjustScroller;
612            if (scroller.isFinished()) {
613                return;
614            }
615        }
616        scroller.computeScrollOffset();
617        int currentScrollerY = scroller.getCurrY();
618        if (mPreviousScrollerY == 0) {
619            mPreviousScrollerY = scroller.getStartY();
620        }
621        scrollBy(0, currentScrollerY - mPreviousScrollerY);
622        mPreviousScrollerY = currentScrollerY;
623        if (scroller.isFinished()) {
624            onScrollerFinished(scroller);
625        } else {
626            invalidate();
627        }
628    }
629
630    /**
631     * Set the enabled state of this view. The interpretation of the enabled
632     * state varies by subclass.
633     *
634     * @param enabled True if this view is enabled, false otherwise.
635     */
636    @Override
637    public void setEnabled(boolean enabled) {
638        super.setEnabled(enabled);
639        mIncrementButton.setEnabled(enabled);
640        mDecrementButton.setEnabled(enabled);
641        mInputText.setEnabled(enabled);
642    }
643
644    /**
645     * Scrolls the selector with the given <code>vertical offset</code>.
646     */
647    @Override
648    public void scrollBy(int x, int y) {
649        int[] selectorIndices = getSelectorIndices();
650        if (mInitialScrollOffset == Integer.MIN_VALUE) {
651            int totalTextHeight = selectorIndices.length * mTextSize;
652            int totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
653            int textGapCount = selectorIndices.length - 1;
654            int selectorTextGapHeight = totalTextGapHeight / textGapCount;
655            // compensate for integer division loss of the components used to
656            // calculate the text gap
657            int integerDivisionLoss = (mTextSize + mBottom - mTop) % textGapCount;
658            mInitialScrollOffset = mCurrentScrollOffset = mTextSize - integerDivisionLoss / 2;
659            mSelectorElementHeight = mTextSize + selectorTextGapHeight;
660        }
661
662        if (!mWrapSelector && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) {
663            mCurrentScrollOffset = mInitialScrollOffset;
664            return;
665        }
666        if (!mWrapSelector && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) {
667            mCurrentScrollOffset = mInitialScrollOffset;
668            return;
669        }
670        mCurrentScrollOffset += y;
671        while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight) {
672            mCurrentScrollOffset -= mSelectorElementHeight;
673            decrementSelectorIndices(selectorIndices);
674            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
675            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) {
676                mCurrentScrollOffset = mInitialScrollOffset;
677            }
678        }
679        while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight) {
680            mCurrentScrollOffset += mSelectorElementHeight;
681            incrementScrollSelectorIndices(selectorIndices);
682            changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
683            if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) {
684                mCurrentScrollOffset = mInitialScrollOffset;
685            }
686        }
687    }
688
689    /**
690     * Set the callback that indicates the number has been adjusted by the user.
691     *
692     * @param listener the callback, should not be null.
693     */
694    public void setOnChangeListener(OnChangedListener listener) {
695        mListener = listener;
696    }
697
698    /**
699     * Set the formatter that will be used to format the number for presentation
700     *
701     * @param formatter the formatter object. If formatter is null,
702     *            String.valueOf() will be used
703     */
704    public void setFormatter(Formatter formatter) {
705        mFormatter = formatter;
706    }
707
708    /**
709     * Set the range of numbers allowed for the number picker. The current value
710     * will be automatically set to the start.
711     *
712     * @param start the start of the range (inclusive)
713     * @param end the end of the range (inclusive)
714     */
715    public void setRange(int start, int end) {
716        setRange(start, end, null);
717    }
718
719    /**
720     * Set the range of numbers allowed for the number picker. The current value
721     * will be automatically set to the start. Also provide a mapping for values
722     * used to display to the user.
723     *
724     * @param start the start of the range (inclusive)
725     * @param end the end of the range (inclusive)
726     * @param displayedValues the values displayed to the user.
727     */
728    public void setRange(int start, int end, String[] displayedValues) {
729        boolean wrapSelector = (end - start) >= mSelectorIndices.length;
730        setRange(start, end, displayedValues, wrapSelector);
731    }
732
733    /**
734     * Set the range of numbers allowed for the number picker. The current value
735     * will be automatically set to the start. Also provide a mapping for values
736     * used to display to the user.
737     *
738     * @param start the start of the range (inclusive)
739     * @param end the end of the range (inclusive)
740     * @param displayedValues the values displayed to the user.
741     */
742    public void setRange(int start, int end, String[] displayedValues, boolean wrapSelector) {
743        if (start < 0 || end < 0) {
744            throw new IllegalArgumentException("start and end must be > 0");
745        }
746
747        mDisplayedValues = displayedValues;
748        mStart = start;
749        mEnd = end;
750        mCurrent = start;
751
752        setWrapSelector(wrapSelector);
753        updateInputTextView();
754
755        if (displayedValues != null) {
756            // Allow text entry rather than strictly numeric entry.
757            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
758                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
759        } else {
760            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
761        }
762
763        // make sure cached string representations are dropped
764        mSelectorIndexToStringCache.clear();
765    }
766
767    /**
768     * Set the current value for the number picker.
769     *
770     * @param current the current value the start of the range (inclusive)
771     * @throws IllegalArgumentException when current is not within the range of
772     *             of the number picker
773     */
774    public void setCurrent(int current) {
775        if (current < mStart || current > mEnd) {
776            throw new IllegalArgumentException("current should be >= start and <= end");
777        }
778        mCurrent = current;
779        updateInputTextView();
780        updateIncrementAndDecrementButtonsVisibilityState();
781    }
782
783    /**
784     * Sets whether the selector shown during flinging/scrolling should wrap
785     * around the beginning and end values.
786     *
787     * @param wrapSelector Whether to wrap.
788     */
789    public void setWrapSelector(boolean wrapSelector) {
790        if (wrapSelector && (mEnd - mStart) < mSelectorIndices.length) {
791            throw new IllegalStateException("Range less than selector items count.");
792        }
793        if (wrapSelector != mWrapSelector) {
794            // force the selector indices array to be reinitialized
795            mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE;
796            mWrapSelector = wrapSelector;
797        }
798    }
799
800    /**
801     * Sets the speed at which the numbers will scroll when the +/- buttons are
802     * longpressed
803     *
804     * @param speed The speed (in milliseconds) at which the numbers will scroll
805     *            default 300ms
806     */
807    public void setSpeed(long speed) {
808        mLongPressUpdateSpeed = speed;
809    }
810
811    /**
812     * Returns the current value of the NumberPicker
813     *
814     * @return the current value.
815     */
816    public int getCurrent() {
817        return mCurrent;
818    }
819
820    @Override
821    public int getSolidColor() {
822        return mSolidColor;
823    }
824
825    @Override
826    protected float getTopFadingEdgeStrength() {
827        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
828    }
829
830    @Override
831    protected float getBottomFadingEdgeStrength() {
832        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
833    }
834
835    @Override
836    protected void onDetachedFromWindow() {
837        removeAllCallbacks();
838    }
839
840    @Override
841    protected void dispatchDraw(Canvas canvas) {
842        // There is a good reason for doing this. See comments in draw().
843    }
844
845    @Override
846    public void draw(Canvas canvas) {
847        // Dispatch draw to our children only if we are not currently running
848        // the animation for simultaneously fading out the scroll wheel and
849        // showing in the buttons. This class takes advantage of the View
850        // implementation of fading edges effect to draw the selector wheel.
851        // However, in View.draw(), the fading is applied after all the children
852        // have been drawn and we do not want this fading to be applied to the
853        // buttons which are currently showing in. Therefore, we draw our
854        // children
855        // after we have completed drawing ourselves.
856
857        // Draw the selector wheel if needed
858        if (mDrawSelectorWheel) {
859            super.draw(canvas);
860        }
861
862        // Draw our children if we are not showing the selector wheel of fading
863        // it out
864        if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) {
865            long drawTime = getDrawingTime();
866            for (int i = 0, count = getChildCount(); i < count; i++) {
867                View child = getChildAt(i);
868                if (!child.isShown()) {
869                    continue;
870                }
871                drawChild(canvas, getChildAt(i), drawTime);
872            }
873        }
874    }
875
876    @Override
877    protected void onDraw(Canvas canvas) {
878        // we only draw the selector wheel
879        if (!mDrawSelectorWheel) {
880            return;
881        }
882        float x = (mRight - mLeft) / 2;
883        float y = mCurrentScrollOffset;
884
885        int[] selectorIndices = getSelectorIndices();
886        for (int i = 0; i < selectorIndices.length; i++) {
887            int selectorIndex = selectorIndices[i];
888            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
889            canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint);
890            y += mSelectorElementHeight;
891        }
892    }
893
894    /**
895     * Returns the upper value of the range of the NumberPicker
896     *
897     * @return the uppper number of the range.
898     */
899    protected int getEndRange() {
900        return mEnd;
901    }
902
903    /**
904     * Returns the lower value of the range of the NumberPicker
905     *
906     * @return the lower number of the range.
907     */
908    protected int getBeginRange() {
909        return mStart;
910    }
911
912    /**
913     * Sets the current value of this NumberPicker, and sets mPrevious to the
914     * previous value. If current is greater than mEnd less than mStart, the
915     * value of mCurrent is wrapped around. Subclasses can override this to
916     * change the wrapping behavior
917     *
918     * @param current the new value of the NumberPicker
919     */
920    private void changeCurrent(int current) {
921        if (mCurrent == current) {
922            return;
923        }
924        // Wrap around the values if we go past the start or end
925        if (mWrapSelector) {
926            current = getWrappedSelectorIndex(current);
927        }
928        int previous = mCurrent;
929        setCurrent(current);
930        notifyChange(previous, current);
931    }
932
933    /**
934     * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
935     * wheel.
936     */
937    @SuppressWarnings("unused")
938    // Called by ShowInputControlsAnimator via reflection
939    private void setSelectorPaintAlpha(int alpha) {
940        mSelectorPaint.setAlpha(alpha);
941        if (mDrawSelectorWheel) {
942            invalidate();
943        }
944    }
945
946    /**
947     * @return If the <code>event</code> is in the input text.
948     */
949    private boolean isEventInInputText(MotionEvent event) {
950        mInputText.getHitRect(mTempRect);
951        return mTempRect.contains((int) event.getX(), (int) event.getY());
952    }
953
954    /**
955     * Sets if to <code>drawSelectionWheel</code>.
956     */
957    private void setDrawSelectorWheel(boolean drawSelectorWheel) {
958        mDrawSelectorWheel = drawSelectorWheel;
959        // do not fade if the selector wheel not shown
960        setVerticalFadingEdgeEnabled(drawSelectorWheel);
961    }
962
963    /**
964     * Callback invoked upon completion of a given <code>scroller</code>.
965     */
966    private void onScrollerFinished(Scroller scroller) {
967        if (scroller == mFlingScroller) {
968            postAdjustScrollerCommand(0);
969        } else {
970            showInputControls();
971            updateInputTextView();
972        }
973    }
974
975    /**
976     * Flings the selector with the given <code>velocityY</code>.
977     */
978    private void fling(int velocityY) {
979        mPreviousScrollerY = 0;
980        Scroller flingScroller = mFlingScroller;
981
982        if (mWrapSelector) {
983            if (velocityY > 0) {
984                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
985            } else {
986                flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
987            }
988        } else {
989            if (velocityY > 0) {
990                int maxY = mTextSize * (mCurrent - mStart);
991                flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY);
992            } else {
993                int startY = mTextSize * (mEnd - mCurrent);
994                int maxY = startY;
995                flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY);
996            }
997        }
998
999        postAdjustScrollerCommand(flingScroller.getDuration());
1000        invalidate();
1001    }
1002
1003    /**
1004     * Hides the input controls which is the up/down arrows and the text field.
1005     */
1006    private void hideInputControls() {
1007        mShowInputControlsAnimator.cancel();
1008        mIncrementButton.setVisibility(INVISIBLE);
1009        mDecrementButton.setVisibility(INVISIBLE);
1010        mInputText.setVisibility(INVISIBLE);
1011    }
1012
1013    /**
1014     * Show the input controls by making them visible and animating the alpha
1015     * property up/down arrows.
1016     */
1017    private void showInputControls() {
1018        updateIncrementAndDecrementButtonsVisibilityState();
1019        mInputText.setVisibility(VISIBLE);
1020        mShowInputControlsAnimator.start();
1021    }
1022
1023    /**
1024     * Updates the visibility state of the increment and decrement buttons.
1025     */
1026    private void updateIncrementAndDecrementButtonsVisibilityState() {
1027        if (mWrapSelector || mCurrent < mEnd) {
1028            mIncrementButton.setVisibility(VISIBLE);
1029        } else {
1030            mIncrementButton.setVisibility(INVISIBLE);
1031        }
1032        if (mWrapSelector || mCurrent > mStart) {
1033            mDecrementButton.setVisibility(VISIBLE);
1034        } else {
1035            mDecrementButton.setVisibility(INVISIBLE);
1036        }
1037    }
1038
1039    /**
1040     * @return The selector indices array with proper values with the current as
1041     *         the middle one.
1042     */
1043    private int[] getSelectorIndices() {
1044        int current = getCurrent();
1045        if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) {
1046            for (int i = 0; i < mSelectorIndices.length; i++) {
1047                int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1048                if (mWrapSelector) {
1049                    selectorIndex = getWrappedSelectorIndex(selectorIndex);
1050                }
1051                mSelectorIndices[i] = selectorIndex;
1052                ensureCachedScrollSelectorValue(mSelectorIndices[i]);
1053            }
1054        }
1055        return mSelectorIndices;
1056    }
1057
1058    /**
1059     * @return The wrapped index <code>selectorIndex</code> value.
1060     *         <p>
1061     *         Note: The absolute value of the argument is never larger than
1062     *         mEnd - mStart.
1063     *         </p>
1064     */
1065    private int getWrappedSelectorIndex(int selectorIndex) {
1066        if (selectorIndex > mEnd) {
1067            return (Math.abs(selectorIndex) - mEnd);
1068        } else if (selectorIndex < mStart) {
1069            return (mEnd - Math.abs(selectorIndex));
1070        }
1071        return selectorIndex;
1072    }
1073
1074    /**
1075     * Increments the <code>selectorIndices</code> whose string representations
1076     * will be displayed in the selector.
1077     */
1078    private void incrementScrollSelectorIndices(int[] selectorIndices) {
1079        for (int i = 0; i < selectorIndices.length - 1; i++) {
1080            selectorIndices[i] = selectorIndices[i + 1];
1081        }
1082        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1083        if (mWrapSelector && nextScrollSelectorIndex > mEnd) {
1084            nextScrollSelectorIndex = mStart;
1085        }
1086        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1087        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1088    }
1089
1090    /**
1091     * Decrements the <code>selectorIndices</code> whose string representations
1092     * will be displayed in the selector.
1093     */
1094    private void decrementSelectorIndices(int[] selectorIndices) {
1095        for (int i = selectorIndices.length - 1; i > 0; i--) {
1096            selectorIndices[i] = selectorIndices[i - 1];
1097        }
1098        int nextScrollSelectorIndex = selectorIndices[1] - 1;
1099        if (mWrapSelector && nextScrollSelectorIndex < mStart) {
1100            nextScrollSelectorIndex = mEnd;
1101        }
1102        selectorIndices[0] = nextScrollSelectorIndex;
1103        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1104    }
1105
1106    /**
1107     * Ensures we have a cached string representation of the given <code>
1108     * selectorIndex</code>
1109     * to avoid multiple instantiations of the same string.
1110     */
1111    private void ensureCachedScrollSelectorValue(int selectorIndex) {
1112        SparseArray<String> cache = mSelectorIndexToStringCache;
1113        String scrollSelectorValue = cache.get(selectorIndex);
1114        if (scrollSelectorValue != null) {
1115            return;
1116        }
1117        if (selectorIndex < mStart || selectorIndex > mEnd) {
1118            scrollSelectorValue = "";
1119        } else {
1120            if (mDisplayedValues != null) {
1121                scrollSelectorValue = mDisplayedValues[selectorIndex];
1122            } else {
1123                scrollSelectorValue = formatNumber(selectorIndex);
1124            }
1125        }
1126        cache.put(selectorIndex, scrollSelectorValue);
1127    }
1128
1129    private String formatNumber(int value) {
1130        return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
1131    }
1132
1133    private void validateInputTextView(View v) {
1134        String str = String.valueOf(((TextView) v).getText());
1135        if (TextUtils.isEmpty(str)) {
1136            // Restore to the old value as we don't allow empty values
1137            updateInputTextView();
1138        } else {
1139            // Check the new value and ensure it's in range
1140            int current = getSelectedPos(str.toString());
1141            changeCurrent(current);
1142        }
1143    }
1144
1145    /**
1146     * Updates the view of this NumberPicker. If displayValues were specified in
1147     * {@link #setRange}, the string corresponding to the index specified by the
1148     * current value will be returned. Otherwise, the formatter specified in
1149     * {@link #setFormatter} will be used to format the number.
1150     */
1151    private void updateInputTextView() {
1152        /*
1153         * If we don't have displayed values then use the current number else
1154         * find the correct value in the displayed values for the current
1155         * number.
1156         */
1157        if (mDisplayedValues == null) {
1158            mInputText.setText(formatNumber(mCurrent));
1159        } else {
1160            mInputText.setText(mDisplayedValues[mCurrent - mStart]);
1161        }
1162        mInputText.setSelection(mInputText.getText().length());
1163    }
1164
1165    /**
1166     * Notifies the listener, if registered, of a change of the value of this
1167     * NumberPicker.
1168     */
1169    private void notifyChange(int previous, int current) {
1170        if (mListener != null) {
1171            mListener.onChanged(this, previous, mCurrent);
1172        }
1173    }
1174
1175    /**
1176     * Posts a command for updating the current value every <code>updateMillis
1177     * </code>.
1178     */
1179    private void postUpdateValueFromLongPress(int updateMillis) {
1180        mInputText.clearFocus();
1181        removeAllCallbacks();
1182        if (mUpdateFromLongPressCommand == null) {
1183            mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand();
1184        }
1185        mUpdateFromLongPressCommand.setUpdateStep(updateMillis);
1186        post(mUpdateFromLongPressCommand);
1187    }
1188
1189    /**
1190     * Removes all pending callback from the message queue.
1191     */
1192    private void removeAllCallbacks() {
1193        if (mUpdateFromLongPressCommand != null) {
1194            removeCallbacks(mUpdateFromLongPressCommand);
1195        }
1196        if (mAdjustScrollerCommand != null) {
1197            removeCallbacks(mAdjustScrollerCommand);
1198        }
1199        if (mSetSelectionCommand != null) {
1200            removeCallbacks(mSetSelectionCommand);
1201        }
1202    }
1203
1204    /**
1205     * @return The selected index given its displayed <code>value</code>.
1206     */
1207    private int getSelectedPos(String value) {
1208        if (mDisplayedValues == null) {
1209            try {
1210                return Integer.parseInt(value);
1211            } catch (NumberFormatException e) {
1212                // Ignore as if it's not a number we don't care
1213            }
1214        } else {
1215            for (int i = 0; i < mDisplayedValues.length; i++) {
1216                // Don't force the user to type in jan when ja will do
1217                value = value.toLowerCase();
1218                if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1219                    return mStart + i;
1220                }
1221            }
1222
1223            /*
1224             * The user might have typed in a number into the month field i.e.
1225             * 10 instead of OCT so support that too.
1226             */
1227            try {
1228                return Integer.parseInt(value);
1229            } catch (NumberFormatException e) {
1230
1231                // Ignore as if it's not a number we don't care
1232            }
1233        }
1234        return mStart;
1235    }
1236
1237    /**
1238     * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1239     * </code> to
1240     * <code>selectionEnd</code>.
1241     */
1242    private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1243        if (mSetSelectionCommand == null) {
1244            mSetSelectionCommand = new SetSelectionCommand();
1245        } else {
1246            removeCallbacks(mSetSelectionCommand);
1247        }
1248        mSetSelectionCommand.mSelectionStart = selectionStart;
1249        mSetSelectionCommand.mSelectionEnd = selectionEnd;
1250        post(mSetSelectionCommand);
1251    }
1252
1253    /**
1254     * Posts an {@link AdjustScrollerCommand} within the given <code>
1255     * delayMillis</code>
1256     * .
1257     */
1258    private void postAdjustScrollerCommand(int delayMillis) {
1259        if (mAdjustScrollerCommand == null) {
1260            mAdjustScrollerCommand = new AdjustScrollerCommand();
1261        } else {
1262            removeCallbacks(mAdjustScrollerCommand);
1263        }
1264        postDelayed(mAdjustScrollerCommand, delayMillis);
1265    }
1266
1267    /**
1268     * Filter for accepting only valid indices or prefixes of the string
1269     * representation of valid indices.
1270     */
1271    class InputTextFilter extends NumberKeyListener {
1272
1273        // XXX This doesn't allow for range limits when controlled by a
1274        // soft input method!
1275        public int getInputType() {
1276            return InputType.TYPE_CLASS_TEXT;
1277        }
1278
1279        @Override
1280        protected char[] getAcceptedChars() {
1281            return DIGIT_CHARACTERS;
1282        }
1283
1284        @Override
1285        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
1286                int dstart, int dend) {
1287            if (mDisplayedValues == null) {
1288                CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1289                if (filtered == null) {
1290                    filtered = source.subSequence(start, end);
1291                }
1292
1293                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1294                        + dest.subSequence(dend, dest.length());
1295
1296                if ("".equals(result)) {
1297                    return result;
1298                }
1299                int val = getSelectedPos(result);
1300
1301                /*
1302                 * Ensure the user can't type in a value greater than the max
1303                 * allowed. We have to allow less than min as the user might
1304                 * want to delete some numbers and then type a new number.
1305                 */
1306                if (val > mEnd) {
1307                    return "";
1308                } else {
1309                    return filtered;
1310                }
1311            } else {
1312                CharSequence filtered = String.valueOf(source.subSequence(start, end));
1313                if (TextUtils.isEmpty(filtered)) {
1314                    return "";
1315                }
1316                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1317                        + dest.subSequence(dend, dest.length());
1318                String str = String.valueOf(result).toLowerCase();
1319                for (String val : mDisplayedValues) {
1320                    String valLowerCase = val.toLowerCase();
1321                    if (valLowerCase.startsWith(str)) {
1322                        postSetSelectionCommand(result.length(), val.length());
1323                        return val.subSequence(dstart, val.length());
1324                    }
1325                }
1326                return "";
1327            }
1328        }
1329    }
1330
1331    /**
1332     * Command for setting the input text selection.
1333     */
1334    class SetSelectionCommand implements Runnable {
1335        private int mSelectionStart;
1336
1337        private int mSelectionEnd;
1338
1339        public void run() {
1340            mInputText.setSelection(mSelectionStart, mSelectionEnd);
1341        }
1342    }
1343
1344    /**
1345     * Command for adjusting the scroller to show in its center the closest of
1346     * the displayed items.
1347     */
1348    class AdjustScrollerCommand implements Runnable {
1349        public void run() {
1350            mPreviousScrollerY = 0;
1351            int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
1352            float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize;
1353            int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS);
1354            mAdjustScroller.startScroll(0, 0, 0, deltaY, duration);
1355            invalidate();
1356        }
1357    }
1358
1359    /**
1360     * Command for updating the current value from a long press.
1361     */
1362    class UpdateValueFromLongPressCommand implements Runnable {
1363        private int mUpdateStep = 0;
1364
1365        private void setUpdateStep(int updateStep) {
1366            mUpdateStep = updateStep;
1367        }
1368
1369        public void run() {
1370            changeCurrent(mCurrent + mUpdateStep);
1371            postDelayed(this, mLongPressUpdateSpeed);
1372        }
1373    }
1374}
1375