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