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