NumberPicker.java revision fb1e80a247221ee7e8f5c5deba04812021d9d07e
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.annotation.Widget;
20import android.content.Context;
21import android.content.res.ColorStateList;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Paint;
26import android.graphics.Paint.Align;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.text.InputFilter;
31import android.text.InputType;
32import android.text.Spanned;
33import android.text.TextUtils;
34import android.text.method.NumberKeyListener;
35import android.util.AttributeSet;
36import android.util.SparseArray;
37import android.util.TypedValue;
38import android.view.KeyEvent;
39import android.view.LayoutInflater;
40import android.view.LayoutInflater.Filter;
41import android.view.MotionEvent;
42import android.view.VelocityTracker;
43import android.view.View;
44import android.view.ViewConfiguration;
45import android.view.accessibility.AccessibilityEvent;
46import android.view.accessibility.AccessibilityManager;
47import android.view.accessibility.AccessibilityNodeInfo;
48import android.view.accessibility.AccessibilityNodeProvider;
49import android.view.animation.DecelerateInterpolator;
50import android.view.inputmethod.EditorInfo;
51import android.view.inputmethod.InputMethodManager;
52
53import com.android.internal.R;
54
55import java.util.ArrayList;
56import java.util.Collections;
57import java.util.List;
58
59/**
60 * A widget that enables the user to select a number form a predefined range.
61 * There are two flavors of this widget and which one is presented to the user
62 * depends on the current theme.
63 * <ul>
64 * <li>
65 * If the current theme is derived from {@link android.R.style#Theme} the widget
66 * presents the current value as an editable input field with an increment button
67 * above and a decrement button below. Long pressing the buttons allows for a quick
68 * change of the current value. Tapping on the input field allows to type in
69 * a desired value.
70 * </li>
71 * <li>
72 * If the current theme is derived from {@link android.R.style#Theme_Holo} or
73 * {@link android.R.style#Theme_Holo_Light} the widget presents the current
74 * value as an editable input field with a lesser value above and a greater
75 * value below. Tapping on the lesser or greater value selects it by animating
76 * the number axis up or down to make the chosen value current. Flinging up
77 * or down allows for multiple increments or decrements of the current value.
78 * Long pressing on the lesser and greater values also allows for a quick change
79 * of the current value. Tapping on the current value allows to type in a
80 * desired value.
81 * </li>
82 * </ul>
83 * <p>
84 * For an example of using this widget, see {@link android.widget.TimePicker}.
85 * </p>
86 */
87@Widget
88public class NumberPicker extends LinearLayout {
89
90    /**
91     * The number of items show in the selector wheel.
92     */
93    private static final int SELECTOR_WHEEL_ITEM_COUNT = 3;
94
95    /**
96     * The default update interval during long press.
97     */
98    private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
99
100    /**
101     * The index of the middle selector item.
102     */
103    private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
104
105    /**
106     * The coefficient by which to adjust (divide) the max fling velocity.
107     */
108    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
109
110    /**
111     * The the duration for adjusting the selector wheel.
112     */
113    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
114
115    /**
116     * The duration of scrolling while snapping to a given position.
117     */
118    private static final int SNAP_SCROLL_DURATION = 300;
119
120    /**
121     * The strength of fading in the top and bottom while drawing the selector.
122     */
123    private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
124
125    /**
126     * The default unscaled height of the selection divider.
127     */
128    private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
129
130    /**
131     * The default unscaled distance between the selection dividers.
132     */
133    private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
134
135    /**
136     * The resource id for the default layout.
137     */
138    private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker;
139
140    /**
141     * The numbers accepted by the input text's {@link Filter}
142     */
143    private static final char[] DIGIT_CHARACTERS = new char[] {
144            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
145    };
146
147    /**
148     * Constant for unspecified size.
149     */
150    private static final int SIZE_UNSPECIFIED = -1;
151
152    /**
153     * Use a custom NumberPicker formatting callback to use two-digit minutes
154     * strings like "01". Keeping a static formatter etc. is the most efficient
155     * way to do this; it avoids creating temporary objects on every call to
156     * format().
157     *
158     * @hide
159     */
160    public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
161        final StringBuilder mBuilder = new StringBuilder();
162
163        final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
164
165        final Object[] mArgs = new Object[1];
166
167        public String format(int value) {
168            mArgs[0] = value;
169            mBuilder.delete(0, mBuilder.length());
170            mFmt.format("%02d", mArgs);
171            return mFmt.toString();
172        }
173    };
174
175    /**
176     * The increment button.
177     */
178    private final ImageButton mIncrementButton;
179
180    /**
181     * The decrement button.
182     */
183    private final ImageButton mDecrementButton;
184
185    /**
186     * The text for showing the current value.
187     */
188    private final EditText mInputText;
189
190    /**
191     * The distance between the two selection dividers.
192     */
193    private final int mSelectionDividersDistance;
194
195    /**
196     * The min height of this widget.
197     */
198    private final int mMinHeight;
199
200    /**
201     * The max height of this widget.
202     */
203    private final int mMaxHeight;
204
205    /**
206     * The max width of this widget.
207     */
208    private final int mMinWidth;
209
210    /**
211     * The max width of this widget.
212     */
213    private int mMaxWidth;
214
215    /**
216     * Flag whether to compute the max width.
217     */
218    private final boolean mComputeMaxWidth;
219
220    /**
221     * The height of the text.
222     */
223    private final int mTextSize;
224
225    /**
226     * The height of the gap between text elements if the selector wheel.
227     */
228    private int mSelectorTextGapHeight;
229
230    /**
231     * The values to be displayed instead the indices.
232     */
233    private String[] mDisplayedValues;
234
235    /**
236     * Lower value of the range of numbers allowed for the NumberPicker
237     */
238    private int mMinValue;
239
240    /**
241     * Upper value of the range of numbers allowed for the NumberPicker
242     */
243    private int mMaxValue;
244
245    /**
246     * Current value of this NumberPicker
247     */
248    private int mValue;
249
250    /**
251     * Listener to be notified upon current value change.
252     */
253    private OnValueChangeListener mOnValueChangeListener;
254
255    /**
256     * Listener to be notified upon scroll state change.
257     */
258    private OnScrollListener mOnScrollListener;
259
260    /**
261     * Formatter for for displaying the current value.
262     */
263    private Formatter mFormatter;
264
265    /**
266     * The speed for updating the value form long press.
267     */
268    private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
269
270    /**
271     * Cache for the string representation of selector indices.
272     */
273    private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
274
275    /**
276     * The selector indices whose value are show by the selector.
277     */
278    private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
279
280    /**
281     * The {@link Paint} for drawing the selector.
282     */
283    private final Paint mSelectorWheelPaint;
284
285    /**
286     * The {@link Drawable} for pressed virtual (increment/decrement) buttons.
287     */
288    private final Drawable mVirtualButtonPressedDrawable;
289
290    /**
291     * The height of a selector element (text + gap).
292     */
293    private int mSelectorElementHeight;
294
295    /**
296     * The initial offset of the scroll selector.
297     */
298    private int mInitialScrollOffset = Integer.MIN_VALUE;
299
300    /**
301     * The current offset of the scroll selector.
302     */
303    private int mCurrentScrollOffset;
304
305    /**
306     * The {@link Scroller} responsible for flinging the selector.
307     */
308    private final Scroller mFlingScroller;
309
310    /**
311     * The {@link Scroller} responsible for adjusting the selector.
312     */
313    private final Scroller mAdjustScroller;
314
315    /**
316     * The previous Y coordinate while scrolling the selector.
317     */
318    private int mPreviousScrollerY;
319
320    /**
321     * Handle to the reusable command for setting the input text selection.
322     */
323    private SetSelectionCommand mSetSelectionCommand;
324
325    /**
326     * Handle to the reusable command for changing the current value from long
327     * press by one.
328     */
329    private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
330
331    /**
332     * Command for beginning an edit of the current value via IME on long press.
333     */
334    private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
335
336    /**
337     * The Y position of the last down event.
338     */
339    private float mLastDownEventY;
340
341    /**
342     * The time of the last down event.
343     */
344    private long mLastDownEventTime;
345
346    /**
347     * The Y position of the last down or move event.
348     */
349    private float mLastDownOrMoveEventY;
350
351    /**
352     * Determines speed during touch scrolling.
353     */
354    private VelocityTracker mVelocityTracker;
355
356    /**
357     * @see ViewConfiguration#getScaledTouchSlop()
358     */
359    private int mTouchSlop;
360
361    /**
362     * @see ViewConfiguration#getScaledMinimumFlingVelocity()
363     */
364    private int mMinimumFlingVelocity;
365
366    /**
367     * @see ViewConfiguration#getScaledMaximumFlingVelocity()
368     */
369    private int mMaximumFlingVelocity;
370
371    /**
372     * Flag whether the selector should wrap around.
373     */
374    private boolean mWrapSelectorWheel;
375
376    /**
377     * The back ground color used to optimize scroller fading.
378     */
379    private final int mSolidColor;
380
381    /**
382     * Flag whether this widget has a selector wheel.
383     */
384    private final boolean mHasSelectorWheel;
385
386    /**
387     * Divider for showing item to be selected while scrolling
388     */
389    private final Drawable mSelectionDivider;
390
391    /**
392     * The height of the selection divider.
393     */
394    private final int mSelectionDividerHeight;
395
396    /**
397     * The current scroll state of the number picker.
398     */
399    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
400
401    /**
402     * Flag whether to ignore move events - we ignore such when we show in IME
403     * to prevent the content from scrolling.
404     */
405    private boolean mIngonreMoveEvents;
406
407    /**
408     * Flag whether to show soft input on tap.
409     */
410    private boolean mShowSoftInputOnTap;
411
412    /**
413     * The top of the top selection divider.
414     */
415    private int mTopSelectionDividerTop;
416
417    /**
418     * The bottom of the bottom selection divider.
419     */
420    private int mBottomSelectionDividerBottom;
421
422    /**
423     * The virtual id of the last hovered child.
424     */
425    private int mLastHoveredChildVirtualViewId;
426
427    /**
428     * Whether the increment virtual button is pressed.
429     */
430    private boolean mIncrementVirtualButtonPressed;
431
432    /**
433     * Whether the decrement virtual button is pressed.
434     */
435    private boolean mDecrementVirtualButtonPressed;
436
437    /**
438     * Provider to report to clients the semantic structure of this widget.
439     */
440    private AccessibilityNodeProviderImpl mAccessibilityNodeProvider;
441
442    /**
443     * Helper class for managing pressed state of the virtual buttons.
444     */
445    private final PressedStateHelper mPressedStateHelper;
446
447    /**
448     * Interface to listen for changes of the current value.
449     */
450    public interface OnValueChangeListener {
451
452        /**
453         * Called upon a change of the current value.
454         *
455         * @param picker The NumberPicker associated with this listener.
456         * @param oldVal The previous value.
457         * @param newVal The new value.
458         */
459        void onValueChange(NumberPicker picker, int oldVal, int newVal);
460    }
461
462    /**
463     * Interface to listen for the picker scroll state.
464     */
465    public interface OnScrollListener {
466
467        /**
468         * The view is not scrolling.
469         */
470        public static int SCROLL_STATE_IDLE = 0;
471
472        /**
473         * The user is scrolling using touch, and his finger is still on the screen.
474         */
475        public static int SCROLL_STATE_TOUCH_SCROLL = 1;
476
477        /**
478         * The user had previously been scrolling using touch and performed a fling.
479         */
480        public static int SCROLL_STATE_FLING = 2;
481
482        /**
483         * Callback invoked while the number picker scroll state has changed.
484         *
485         * @param view The view whose scroll state is being reported.
486         * @param scrollState The current scroll state. One of
487         *            {@link #SCROLL_STATE_IDLE},
488         *            {@link #SCROLL_STATE_TOUCH_SCROLL} or
489         *            {@link #SCROLL_STATE_IDLE}.
490         */
491        public void onScrollStateChange(NumberPicker view, int scrollState);
492    }
493
494    /**
495     * Interface used to format current value into a string for presentation.
496     */
497    public interface Formatter {
498
499        /**
500         * Formats a string representation of the current value.
501         *
502         * @param value The currently selected value.
503         * @return A formatted string representation.
504         */
505        public String format(int value);
506    }
507
508    /**
509     * Create a new number picker.
510     *
511     * @param context The application environment.
512     */
513    public NumberPicker(Context context) {
514        this(context, null);
515    }
516
517    /**
518     * Create a new number picker.
519     *
520     * @param context The application environment.
521     * @param attrs A collection of attributes.
522     */
523    public NumberPicker(Context context, AttributeSet attrs) {
524        this(context, attrs, R.attr.numberPickerStyle);
525    }
526
527    /**
528     * Create a new number picker
529     *
530     * @param context the application environment.
531     * @param attrs a collection of attributes.
532     * @param defStyle The default style to apply to this view.
533     */
534    public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
535        super(context, attrs, defStyle);
536
537        // process style attributes
538        TypedArray attributesArray = context.obtainStyledAttributes(
539                attrs, R.styleable.NumberPicker, defStyle, 0);
540        final int layoutResId = attributesArray.getResourceId(
541                R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID);
542
543        mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
544
545        mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
546
547        mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider);
548
549        final int defSelectionDividerHeight = (int) TypedValue.applyDimension(
550                TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
551                getResources().getDisplayMetrics());
552        mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
553                R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight);
554
555        final int defSelectionDividerDistance = (int) TypedValue.applyDimension(
556                TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE,
557                getResources().getDisplayMetrics());
558        mSelectionDividersDistance = attributesArray.getDimensionPixelSize(
559                R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance);
560
561        mMinHeight = attributesArray.getDimensionPixelSize(
562                R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED);
563
564        mMaxHeight = attributesArray.getDimensionPixelSize(
565                R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED);
566        if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
567                && mMinHeight > mMaxHeight) {
568            throw new IllegalArgumentException("minHeight > maxHeight");
569        }
570
571        mMinWidth = attributesArray.getDimensionPixelSize(
572                R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED);
573
574        mMaxWidth = attributesArray.getDimensionPixelSize(
575                R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED);
576        if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
577                && mMinWidth > mMaxWidth) {
578            throw new IllegalArgumentException("minWidth > maxWidth");
579        }
580
581        mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
582
583        mVirtualButtonPressedDrawable = attributesArray.getDrawable(
584                R.styleable.NumberPicker_virtualButtonPressedDrawable);
585
586        attributesArray.recycle();
587
588        mPressedStateHelper = new PressedStateHelper();
589
590        // By default Linearlayout that we extend is not drawn. This is
591        // its draw() method is not called but dispatchDraw() is called
592        // directly (see ViewGroup.drawChild()). However, this class uses
593        // the fading edge effect implemented by View and we need our
594        // draw() method to be called. Therefore, we declare we will draw.
595        setWillNotDraw(!mHasSelectorWheel);
596
597        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
598                Context.LAYOUT_INFLATER_SERVICE);
599        inflater.inflate(layoutResId, this, true);
600
601        OnClickListener onClickListener = new OnClickListener() {
602            public void onClick(View v) {
603                hideSoftInput();
604                mInputText.clearFocus();
605                if (v.getId() == R.id.increment) {
606                    changeValueByOne(true);
607                } else {
608                    changeValueByOne(false);
609                }
610            }
611        };
612
613        OnLongClickListener onLongClickListener = new OnLongClickListener() {
614            public boolean onLongClick(View v) {
615                hideSoftInput();
616                mInputText.clearFocus();
617                if (v.getId() == R.id.increment) {
618                    postChangeCurrentByOneFromLongPress(true, 0);
619                } else {
620                    postChangeCurrentByOneFromLongPress(false, 0);
621                }
622                return true;
623            }
624        };
625
626        // increment button
627        if (!mHasSelectorWheel) {
628            mIncrementButton = (ImageButton) findViewById(R.id.increment);
629            mIncrementButton.setOnClickListener(onClickListener);
630            mIncrementButton.setOnLongClickListener(onLongClickListener);
631        } else {
632            mIncrementButton = null;
633        }
634
635        // decrement button
636        if (!mHasSelectorWheel) {
637            mDecrementButton = (ImageButton) findViewById(R.id.decrement);
638            mDecrementButton.setOnClickListener(onClickListener);
639            mDecrementButton.setOnLongClickListener(onLongClickListener);
640        } else {
641            mDecrementButton = null;
642        }
643
644        // input text
645        mInputText = (EditText) findViewById(R.id.numberpicker_input);
646        mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
647            public void onFocusChange(View v, boolean hasFocus) {
648                if (hasFocus) {
649                    mInputText.selectAll();
650                } else {
651                    mInputText.setSelection(0, 0);
652                    validateInputTextView(v);
653                }
654            }
655        });
656        mInputText.setFilters(new InputFilter[] {
657            new InputTextFilter()
658        });
659
660        mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
661        mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
662
663        // initialize constants
664        ViewConfiguration configuration = ViewConfiguration.get(context);
665        mTouchSlop = configuration.getScaledTouchSlop();
666        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
667        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
668                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
669        mTextSize = (int) mInputText.getTextSize();
670
671        // create the selector wheel paint
672        Paint paint = new Paint();
673        paint.setAntiAlias(true);
674        paint.setTextAlign(Align.CENTER);
675        paint.setTextSize(mTextSize);
676        paint.setTypeface(mInputText.getTypeface());
677        ColorStateList colors = mInputText.getTextColors();
678        int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
679        paint.setColor(color);
680        mSelectorWheelPaint = paint;
681
682        // create the fling and adjust scrollers
683        mFlingScroller = new Scroller(getContext(), null, true);
684        mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
685
686        updateInputTextView();
687
688        // If not explicitly specified this view is important for accessibility.
689        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
690            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
691        }
692    }
693
694    @Override
695    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
696        if (!mHasSelectorWheel) {
697            super.onLayout(changed, left, top, right, bottom);
698            return;
699        }
700        final int msrdWdth = getMeasuredWidth();
701        final int msrdHght = getMeasuredHeight();
702
703        // Input text centered horizontally.
704        final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
705        final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
706        final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
707        final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
708        final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
709        final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
710        mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
711
712        if (changed) {
713            // need to do all this when we know our size
714            initializeSelectorWheel();
715            initializeFadingEdges();
716            mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
717                    - mSelectionDividerHeight;
718            mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
719                    + mSelectionDividersDistance;
720        }
721    }
722
723    @Override
724    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
725        if (!mHasSelectorWheel) {
726            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
727            return;
728        }
729        // Try greedily to fit the max width and height.
730        final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
731        final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
732        super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
733        // Flag if we are measured with width or height less than the respective min.
734        final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
735                widthMeasureSpec);
736        final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
737                heightMeasureSpec);
738        setMeasuredDimension(widthSize, heightSize);
739    }
740
741    /**
742     * Move to the final position of a scroller. Ensures to force finish the scroller
743     * and if it is not at its final position a scroll of the selector wheel is
744     * performed to fast forward to the final position.
745     *
746     * @param scroller The scroller to whose final position to get.
747     * @return True of the a move was performed, i.e. the scroller was not in final position.
748     */
749    private boolean moveToFinalScrollerPosition(Scroller scroller) {
750        scroller.forceFinished(true);
751        int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
752        int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight;
753        int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
754        if (overshootAdjustment != 0) {
755            if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) {
756                if (overshootAdjustment > 0) {
757                    overshootAdjustment -= mSelectorElementHeight;
758                } else {
759                    overshootAdjustment += mSelectorElementHeight;
760                }
761            }
762            amountToScroll += overshootAdjustment;
763            scrollBy(0, amountToScroll);
764            return true;
765        }
766        return false;
767    }
768
769    @Override
770    public boolean onInterceptTouchEvent(MotionEvent event) {
771        if (!mHasSelectorWheel || !isEnabled()) {
772            return false;
773        }
774        final int action = event.getActionMasked();
775        switch (action) {
776            case MotionEvent.ACTION_DOWN: {
777                removeAllCallbacks();
778                mInputText.setVisibility(View.INVISIBLE);
779                mLastDownOrMoveEventY = mLastDownEventY = event.getY();
780                mLastDownEventTime = event.getEventTime();
781                mIngonreMoveEvents = false;
782                mShowSoftInputOnTap = false;
783                // Handle pressed state before any state change.
784                if (mLastDownEventY < mTopSelectionDividerTop) {
785                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
786                        mPressedStateHelper.buttonPressDelayed(
787                                PressedStateHelper.BUTTON_DECREMENT);
788                    }
789                } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
790                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
791                        mPressedStateHelper.buttonPressDelayed(
792                                PressedStateHelper.BUTTON_INCREMENT);
793                    }
794                }
795                // Make sure we support flinging inside scrollables.
796                getParent().requestDisallowInterceptTouchEvent(true);
797                if (!mFlingScroller.isFinished()) {
798                    mFlingScroller.forceFinished(true);
799                    mAdjustScroller.forceFinished(true);
800                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
801                } else if (!mAdjustScroller.isFinished()) {
802                    mFlingScroller.forceFinished(true);
803                    mAdjustScroller.forceFinished(true);
804                } else if (mLastDownEventY < mTopSelectionDividerTop) {
805                    hideSoftInput();
806                    postChangeCurrentByOneFromLongPress(
807                            false, ViewConfiguration.getLongPressTimeout());
808                } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
809                    hideSoftInput();
810                    postChangeCurrentByOneFromLongPress(
811                            true, ViewConfiguration.getLongPressTimeout());
812                } else {
813                    mShowSoftInputOnTap = true;
814                    postBeginSoftInputOnLongPressCommand();
815                }
816                return true;
817            }
818        }
819        return false;
820    }
821
822    @Override
823    public boolean onTouchEvent(MotionEvent event) {
824        if (!isEnabled() || !mHasSelectorWheel) {
825            return false;
826        }
827        if (mVelocityTracker == null) {
828            mVelocityTracker = VelocityTracker.obtain();
829        }
830        mVelocityTracker.addMovement(event);
831        int action = event.getActionMasked();
832        switch (action) {
833            case MotionEvent.ACTION_MOVE: {
834                if (mIngonreMoveEvents) {
835                    break;
836                }
837                float currentMoveY = event.getY();
838                if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
839                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
840                    if (deltaDownY > mTouchSlop) {
841                        removeAllCallbacks();
842                        onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
843                    }
844                } else {
845                    int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
846                    scrollBy(0, deltaMoveY);
847                    invalidate();
848                }
849                mLastDownOrMoveEventY = currentMoveY;
850            } break;
851            case MotionEvent.ACTION_UP: {
852                removeBeginSoftInputCommand();
853                removeChangeCurrentByOneFromLongPress();
854                mPressedStateHelper.cancel();
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                    int eventY = (int) event.getY();
863                    int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
864                    long deltaTime = event.getEventTime() - mLastDownEventTime;
865                    if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
866                        if (mShowSoftInputOnTap) {
867                            mShowSoftInputOnTap = false;
868                            showSoftInput();
869                        } else {
870                            int selectorIndexOffset = (eventY / mSelectorElementHeight)
871                                    - SELECTOR_MIDDLE_ITEM_INDEX;
872                            if (selectorIndexOffset > 0) {
873                                changeValueByOne(true);
874                                mPressedStateHelper.buttonTapped(
875                                        PressedStateHelper.BUTTON_INCREMENT);
876                            } else if (selectorIndexOffset < 0) {
877                                changeValueByOne(false);
878                                mPressedStateHelper.buttonTapped(
879                                        PressedStateHelper.BUTTON_DECREMENT);
880                            }
881                        }
882                    } else {
883                        ensureScrollWheelAdjusted();
884                    }
885                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
886                }
887                mVelocityTracker.recycle();
888                mVelocityTracker = null;
889            } break;
890        }
891        return true;
892    }
893
894    @Override
895    public boolean dispatchTouchEvent(MotionEvent event) {
896        final int action = event.getActionMasked();
897        switch (action) {
898            case MotionEvent.ACTION_CANCEL:
899            case MotionEvent.ACTION_UP:
900                removeAllCallbacks();
901                break;
902        }
903        return super.dispatchTouchEvent(event);
904    }
905
906    @Override
907    public boolean dispatchKeyEvent(KeyEvent event) {
908        final int keyCode = event.getKeyCode();
909        switch (keyCode) {
910            case KeyEvent.KEYCODE_DPAD_CENTER:
911            case KeyEvent.KEYCODE_ENTER:
912                removeAllCallbacks();
913                break;
914        }
915        return super.dispatchKeyEvent(event);
916    }
917
918    @Override
919    public boolean dispatchTrackballEvent(MotionEvent event) {
920        final int action = event.getActionMasked();
921        switch (action) {
922            case MotionEvent.ACTION_CANCEL:
923            case MotionEvent.ACTION_UP:
924                removeAllCallbacks();
925                break;
926        }
927        return super.dispatchTrackballEvent(event);
928    }
929
930    @Override
931    protected boolean dispatchHoverEvent(MotionEvent event) {
932        if (!mHasSelectorWheel) {
933            return super.dispatchHoverEvent(event);
934        }
935        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
936            final int eventY = (int) event.getY();
937            final int hoveredVirtualViewId;
938            if (eventY < mTopSelectionDividerTop) {
939                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
940            } else if (eventY > mBottomSelectionDividerBottom) {
941                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
942            } else {
943                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
944            }
945            final int action = event.getActionMasked();
946            AccessibilityNodeProviderImpl provider =
947                (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider();
948            switch (action) {
949                case MotionEvent.ACTION_HOVER_ENTER: {
950                    provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
951                            AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
952                    mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
953                    provider.performAction(hoveredVirtualViewId,
954                            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
955                } break;
956                case MotionEvent.ACTION_HOVER_MOVE: {
957                    if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
958                            && mLastHoveredChildVirtualViewId != View.NO_ID) {
959                        provider.sendAccessibilityEventForVirtualView(
960                                mLastHoveredChildVirtualViewId,
961                                AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
962                        provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
963                                AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
964                        mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
965                        provider.performAction(hoveredVirtualViewId,
966                                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
967                    }
968                } break;
969                case MotionEvent.ACTION_HOVER_EXIT: {
970                    provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
971                            AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
972                    mLastHoveredChildVirtualViewId = View.NO_ID;
973                } break;
974            }
975        }
976        return false;
977    }
978
979    @Override
980    public void computeScroll() {
981        Scroller scroller = mFlingScroller;
982        if (scroller.isFinished()) {
983            scroller = mAdjustScroller;
984            if (scroller.isFinished()) {
985                return;
986            }
987        }
988        scroller.computeScrollOffset();
989        int currentScrollerY = scroller.getCurrY();
990        if (mPreviousScrollerY == 0) {
991            mPreviousScrollerY = scroller.getStartY();
992        }
993        scrollBy(0, currentScrollerY - mPreviousScrollerY);
994        mPreviousScrollerY = currentScrollerY;
995        if (scroller.isFinished()) {
996            onScrollerFinished(scroller);
997        } else {
998            invalidate();
999        }
1000    }
1001
1002    @Override
1003    public void setEnabled(boolean enabled) {
1004        super.setEnabled(enabled);
1005        if (!mHasSelectorWheel) {
1006            mIncrementButton.setEnabled(enabled);
1007        }
1008        if (!mHasSelectorWheel) {
1009            mDecrementButton.setEnabled(enabled);
1010        }
1011        mInputText.setEnabled(enabled);
1012    }
1013
1014    @Override
1015    public void scrollBy(int x, int y) {
1016        int[] selectorIndices = mSelectorIndices;
1017        if (!mWrapSelectorWheel && y > 0
1018                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
1019            mCurrentScrollOffset = mInitialScrollOffset;
1020            return;
1021        }
1022        if (!mWrapSelectorWheel && y < 0
1023                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
1024            mCurrentScrollOffset = mInitialScrollOffset;
1025            return;
1026        }
1027        mCurrentScrollOffset += y;
1028        while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
1029            mCurrentScrollOffset -= mSelectorElementHeight;
1030            decrementSelectorIndices(selectorIndices);
1031            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
1032            if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
1033                mCurrentScrollOffset = mInitialScrollOffset;
1034            }
1035        }
1036        while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
1037            mCurrentScrollOffset += mSelectorElementHeight;
1038            incrementSelectorIndices(selectorIndices);
1039            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
1040            if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
1041                mCurrentScrollOffset = mInitialScrollOffset;
1042            }
1043        }
1044    }
1045
1046    @Override
1047    public int getSolidColor() {
1048        return mSolidColor;
1049    }
1050
1051    /**
1052     * Sets the listener to be notified on change of the current value.
1053     *
1054     * @param onValueChangedListener The listener.
1055     */
1056    public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
1057        mOnValueChangeListener = onValueChangedListener;
1058    }
1059
1060    /**
1061     * Set listener to be notified for scroll state changes.
1062     *
1063     * @param onScrollListener The listener.
1064     */
1065    public void setOnScrollListener(OnScrollListener onScrollListener) {
1066        mOnScrollListener = onScrollListener;
1067    }
1068
1069    /**
1070     * Set the formatter to be used for formatting the current value.
1071     * <p>
1072     * Note: If you have provided alternative values for the values this
1073     * formatter is never invoked.
1074     * </p>
1075     *
1076     * @param formatter The formatter object. If formatter is <code>null</code>,
1077     *            {@link String#valueOf(int)} will be used.
1078     *@see #setDisplayedValues(String[])
1079     */
1080    public void setFormatter(Formatter formatter) {
1081        if (formatter == mFormatter) {
1082            return;
1083        }
1084        mFormatter = formatter;
1085        initializeSelectorWheelIndices();
1086        updateInputTextView();
1087    }
1088
1089    /**
1090     * Set the current value for the number picker.
1091     * <p>
1092     * If the argument is less than the {@link NumberPicker#getMinValue()} and
1093     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
1094     * current value is set to the {@link NumberPicker#getMinValue()} value.
1095     * </p>
1096     * <p>
1097     * If the argument is less than the {@link NumberPicker#getMinValue()} and
1098     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
1099     * current value is set to the {@link NumberPicker#getMaxValue()} value.
1100     * </p>
1101     * <p>
1102     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
1103     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
1104     * current value is set to the {@link NumberPicker#getMaxValue()} value.
1105     * </p>
1106     * <p>
1107     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
1108     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
1109     * current value is set to the {@link NumberPicker#getMinValue()} value.
1110     * </p>
1111     *
1112     * @param value The current value.
1113     * @see #setWrapSelectorWheel(boolean)
1114     * @see #setMinValue(int)
1115     * @see #setMaxValue(int)
1116     */
1117    public void setValue(int value) {
1118        setValueInternal(value, false);
1119    }
1120
1121    /**
1122     * Shows the soft input for its input text.
1123     */
1124    private void showSoftInput() {
1125        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
1126        if (inputMethodManager != null) {
1127            if (mHasSelectorWheel) {
1128                mInputText.setVisibility(View.VISIBLE);
1129            }
1130            mInputText.requestFocus();
1131            inputMethodManager.showSoftInput(mInputText, 0);
1132        }
1133    }
1134
1135    /**
1136     * Hides the soft input if it is active for the input text.
1137     */
1138    private void hideSoftInput() {
1139        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
1140        if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
1141            inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
1142            if (mHasSelectorWheel) {
1143                mInputText.setVisibility(View.INVISIBLE);
1144            }
1145        }
1146    }
1147
1148    /**
1149     * Computes the max width if no such specified as an attribute.
1150     */
1151    private void tryComputeMaxWidth() {
1152        if (!mComputeMaxWidth) {
1153            return;
1154        }
1155        int maxTextWidth = 0;
1156        if (mDisplayedValues == null) {
1157            float maxDigitWidth = 0;
1158            for (int i = 0; i <= 9; i++) {
1159                final float digitWidth = mSelectorWheelPaint.measureText(String.valueOf(i));
1160                if (digitWidth > maxDigitWidth) {
1161                    maxDigitWidth = digitWidth;
1162                }
1163            }
1164            int numberOfDigits = 0;
1165            int current = mMaxValue;
1166            while (current > 0) {
1167                numberOfDigits++;
1168                current = current / 10;
1169            }
1170            maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
1171        } else {
1172            final int valueCount = mDisplayedValues.length;
1173            for (int i = 0; i < valueCount; i++) {
1174                final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
1175                if (textWidth > maxTextWidth) {
1176                    maxTextWidth = (int) textWidth;
1177                }
1178            }
1179        }
1180        maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
1181        if (mMaxWidth != maxTextWidth) {
1182            if (maxTextWidth > mMinWidth) {
1183                mMaxWidth = maxTextWidth;
1184            } else {
1185                mMaxWidth = mMinWidth;
1186            }
1187            invalidate();
1188        }
1189    }
1190
1191    /**
1192     * Gets whether the selector wheel wraps when reaching the min/max value.
1193     *
1194     * @return True if the selector wheel wraps.
1195     *
1196     * @see #getMinValue()
1197     * @see #getMaxValue()
1198     */
1199    public boolean getWrapSelectorWheel() {
1200        return mWrapSelectorWheel;
1201    }
1202
1203    /**
1204     * Sets whether the selector wheel shown during flinging/scrolling should
1205     * wrap around the {@link NumberPicker#getMinValue()} and
1206     * {@link NumberPicker#getMaxValue()} values.
1207     * <p>
1208     * By default if the range (max - min) is more than the number of items shown
1209     * on the selector wheel the selector wheel wrapping is enabled.
1210     * </p>
1211     * <p>
1212     * <strong>Note:</strong> If the number of items, i.e. the range (
1213     * {@link #getMaxValue()} - {@link #getMinValue()}) is less than
1214     * the number of items shown on the selector wheel, the selector wheel will
1215     * not wrap. Hence, in such a case calling this method is a NOP.
1216     * </p>
1217     *
1218     * @param wrapSelectorWheel Whether to wrap.
1219     */
1220    public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
1221        final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
1222        if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) {
1223            mWrapSelectorWheel = wrapSelectorWheel;
1224        }
1225    }
1226
1227    /**
1228     * Sets the speed at which the numbers be incremented and decremented when
1229     * the up and down buttons are long pressed respectively.
1230     * <p>
1231     * The default value is 300 ms.
1232     * </p>
1233     *
1234     * @param intervalMillis The speed (in milliseconds) at which the numbers
1235     *            will be incremented and decremented.
1236     */
1237    public void setOnLongPressUpdateInterval(long intervalMillis) {
1238        mLongPressUpdateInterval = intervalMillis;
1239    }
1240
1241    /**
1242     * Returns the value of the picker.
1243     *
1244     * @return The value.
1245     */
1246    public int getValue() {
1247        return mValue;
1248    }
1249
1250    /**
1251     * Returns the min value of the picker.
1252     *
1253     * @return The min value
1254     */
1255    public int getMinValue() {
1256        return mMinValue;
1257    }
1258
1259    /**
1260     * Sets the min value of the picker.
1261     *
1262     * @param minValue The min value.
1263     */
1264    public void setMinValue(int minValue) {
1265        if (mMinValue == minValue) {
1266            return;
1267        }
1268        if (minValue < 0) {
1269            throw new IllegalArgumentException("minValue must be >= 0");
1270        }
1271        mMinValue = minValue;
1272        if (mMinValue > mValue) {
1273            mValue = mMinValue;
1274        }
1275        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1276        setWrapSelectorWheel(wrapSelectorWheel);
1277        initializeSelectorWheelIndices();
1278        updateInputTextView();
1279        tryComputeMaxWidth();
1280        invalidate();
1281    }
1282
1283    /**
1284     * Returns the max value of the picker.
1285     *
1286     * @return The max value.
1287     */
1288    public int getMaxValue() {
1289        return mMaxValue;
1290    }
1291
1292    /**
1293     * Sets the max value of the picker.
1294     *
1295     * @param maxValue The max value.
1296     */
1297    public void setMaxValue(int maxValue) {
1298        if (mMaxValue == maxValue) {
1299            return;
1300        }
1301        if (maxValue < 0) {
1302            throw new IllegalArgumentException("maxValue must be >= 0");
1303        }
1304        mMaxValue = maxValue;
1305        if (mMaxValue < mValue) {
1306            mValue = mMaxValue;
1307        }
1308        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1309        setWrapSelectorWheel(wrapSelectorWheel);
1310        initializeSelectorWheelIndices();
1311        updateInputTextView();
1312        tryComputeMaxWidth();
1313        invalidate();
1314    }
1315
1316    /**
1317     * Gets the values to be displayed instead of string values.
1318     *
1319     * @return The displayed values.
1320     */
1321    public String[] getDisplayedValues() {
1322        return mDisplayedValues;
1323    }
1324
1325    /**
1326     * Sets the values to be displayed.
1327     *
1328     * @param displayedValues The displayed values.
1329     */
1330    public void setDisplayedValues(String[] displayedValues) {
1331        if (mDisplayedValues == displayedValues) {
1332            return;
1333        }
1334        mDisplayedValues = displayedValues;
1335        if (mDisplayedValues != null) {
1336            // Allow text entry rather than strictly numeric entry.
1337            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
1338                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
1339        } else {
1340            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1341        }
1342        updateInputTextView();
1343        initializeSelectorWheelIndices();
1344        tryComputeMaxWidth();
1345    }
1346
1347    @Override
1348    protected float getTopFadingEdgeStrength() {
1349        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1350    }
1351
1352    @Override
1353    protected float getBottomFadingEdgeStrength() {
1354        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1355    }
1356
1357    @Override
1358    protected void onDetachedFromWindow() {
1359        removeAllCallbacks();
1360    }
1361
1362    @Override
1363    protected void onDraw(Canvas canvas) {
1364        if (!mHasSelectorWheel) {
1365            super.onDraw(canvas);
1366            return;
1367        }
1368        float x = (mRight - mLeft) / 2;
1369        float y = mCurrentScrollOffset;
1370
1371        // draw the virtual buttons pressed state if needed
1372        if (mVirtualButtonPressedDrawable != null
1373                && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
1374            if (mDecrementVirtualButtonPressed) {
1375                mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
1376                mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop);
1377                mVirtualButtonPressedDrawable.draw(canvas);
1378            }
1379            if (mIncrementVirtualButtonPressed) {
1380                mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
1381                mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight,
1382                        mBottom);
1383                mVirtualButtonPressedDrawable.draw(canvas);
1384            }
1385        }
1386
1387        // draw the selector wheel
1388        int[] selectorIndices = mSelectorIndices;
1389        for (int i = 0; i < selectorIndices.length; i++) {
1390            int selectorIndex = selectorIndices[i];
1391            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1392            // Do not draw the middle item if input is visible since the input
1393            // is shown only if the wheel is static and it covers the middle
1394            // item. Otherwise, if the user starts editing the text via the
1395            // IME he may see a dimmed version of the old value intermixed
1396            // with the new one.
1397            if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) {
1398                canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
1399            }
1400            y += mSelectorElementHeight;
1401        }
1402
1403        // draw the selection dividers
1404        if (mSelectionDivider != null) {
1405            // draw the top divider
1406            int topOfTopDivider = mTopSelectionDividerTop;
1407            int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
1408            mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
1409            mSelectionDivider.draw(canvas);
1410
1411            // draw the bottom divider
1412            int bottomOfBottomDivider = mBottomSelectionDividerBottom;
1413            int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
1414            mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
1415            mSelectionDivider.draw(canvas);
1416        }
1417    }
1418
1419    @Override
1420    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
1421        // We do not want the real descendant to be considered focus search
1422        // since it is managed by the accessibility node provider.
1423        if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
1424            if (canTakeAccessibilityFocusFromHover() || getAccessibilityNodeProvider() != null) {
1425                views.add(this);
1426                return;
1427            }
1428        }
1429        super.addFocusables(views, direction, focusableMode);
1430    }
1431
1432    @Override
1433    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1434        super.onInitializeAccessibilityEvent(event);
1435        event.setClassName(NumberPicker.class.getName());
1436        event.setScrollable(true);
1437        event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
1438        event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
1439    }
1440
1441    @Override
1442    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
1443        if (!mHasSelectorWheel) {
1444            return super.getAccessibilityNodeProvider();
1445        }
1446        if (mAccessibilityNodeProvider == null) {
1447            mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
1448        }
1449        return mAccessibilityNodeProvider;
1450    }
1451
1452    /**
1453     * Makes a measure spec that tries greedily to use the max value.
1454     *
1455     * @param measureSpec The measure spec.
1456     * @param maxSize The max value for the size.
1457     * @return A measure spec greedily imposing the max size.
1458     */
1459    private int makeMeasureSpec(int measureSpec, int maxSize) {
1460        if (maxSize == SIZE_UNSPECIFIED) {
1461            return measureSpec;
1462        }
1463        final int size = MeasureSpec.getSize(measureSpec);
1464        final int mode = MeasureSpec.getMode(measureSpec);
1465        switch (mode) {
1466            case MeasureSpec.EXACTLY:
1467                return measureSpec;
1468            case MeasureSpec.AT_MOST:
1469                return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
1470            case MeasureSpec.UNSPECIFIED:
1471                return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
1472            default:
1473                throw new IllegalArgumentException("Unknown measure mode: " + mode);
1474        }
1475    }
1476
1477    /**
1478     * Utility to reconcile a desired size and state, with constraints imposed
1479     * by a MeasureSpec. Tries to respect the min size, unless a different size
1480     * is imposed by the constraints.
1481     *
1482     * @param minSize The minimal desired size.
1483     * @param measuredSize The currently measured size.
1484     * @param measureSpec The current measure spec.
1485     * @return The resolved size and state.
1486     */
1487    private int resolveSizeAndStateRespectingMinSize(
1488            int minSize, int measuredSize, int measureSpec) {
1489        if (minSize != SIZE_UNSPECIFIED) {
1490            final int desiredWidth = Math.max(minSize, measuredSize);
1491            return resolveSizeAndState(desiredWidth, measureSpec, 0);
1492        } else {
1493            return measuredSize;
1494        }
1495    }
1496
1497    /**
1498     * Resets the selector indices and clear the cached string representation of
1499     * these indices.
1500     */
1501    private void initializeSelectorWheelIndices() {
1502        mSelectorIndexToStringCache.clear();
1503        int[] selectorIndices = mSelectorIndices;
1504        int current = getValue();
1505        for (int i = 0; i < mSelectorIndices.length; i++) {
1506            int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1507            if (mWrapSelectorWheel) {
1508                selectorIndex = getWrappedSelectorIndex(selectorIndex);
1509            }
1510            selectorIndices[i] = selectorIndex;
1511            ensureCachedScrollSelectorValue(selectorIndices[i]);
1512        }
1513    }
1514
1515    /**
1516     * Sets the current value of this NumberPicker.
1517     *
1518     * @param current The new value of the NumberPicker.
1519     * @param notifyChange Whether to notify if the current value changed.
1520     */
1521    private void setValueInternal(int current, boolean notifyChange) {
1522        if (mValue == current) {
1523            return;
1524        }
1525        // Wrap around the values if we go past the start or end
1526        if (mWrapSelectorWheel) {
1527            current = getWrappedSelectorIndex(current);
1528        } else {
1529            current = Math.max(current, mMinValue);
1530            current = Math.min(current, mMaxValue);
1531        }
1532        int previous = mValue;
1533        mValue = current;
1534        updateInputTextView();
1535        if (notifyChange) {
1536            notifyChange(previous, current);
1537        }
1538        initializeSelectorWheelIndices();
1539        invalidate();
1540    }
1541
1542    /**
1543     * Changes the current value by one which is increment or
1544     * decrement based on the passes argument.
1545     * decrement the current value.
1546     *
1547     * @param increment True to increment, false to decrement.
1548     */
1549     private void changeValueByOne(boolean increment) {
1550        if (mHasSelectorWheel) {
1551            mInputText.setVisibility(View.INVISIBLE);
1552            if (!moveToFinalScrollerPosition(mFlingScroller)) {
1553                moveToFinalScrollerPosition(mAdjustScroller);
1554            }
1555            mPreviousScrollerY = 0;
1556            if (increment) {
1557                mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION);
1558            } else {
1559                mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION);
1560            }
1561            invalidate();
1562        } else {
1563            if (increment) {
1564                setValueInternal(mValue + 1, true);
1565            } else {
1566                setValueInternal(mValue - 1, true);
1567            }
1568        }
1569    }
1570
1571    private void initializeSelectorWheel() {
1572        initializeSelectorWheelIndices();
1573        int[] selectorIndices = mSelectorIndices;
1574        int totalTextHeight = selectorIndices.length * mTextSize;
1575        float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
1576        float textGapCount = selectorIndices.length;
1577        mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
1578        mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
1579        // Ensure that the middle item is positioned the same as the text in
1580        // mInputText
1581        int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
1582        mInitialScrollOffset = editTextTextPosition
1583                - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
1584        mCurrentScrollOffset = mInitialScrollOffset;
1585        updateInputTextView();
1586    }
1587
1588    private void initializeFadingEdges() {
1589        setVerticalFadingEdgeEnabled(true);
1590        setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
1591    }
1592
1593    /**
1594     * Callback invoked upon completion of a given <code>scroller</code>.
1595     */
1596    private void onScrollerFinished(Scroller scroller) {
1597        if (scroller == mFlingScroller) {
1598            if (!ensureScrollWheelAdjusted()) {
1599                updateInputTextView();
1600            }
1601            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1602        } else {
1603            if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
1604                updateInputTextView();
1605            }
1606        }
1607    }
1608
1609    /**
1610     * Handles transition to a given <code>scrollState</code>
1611     */
1612    private void onScrollStateChange(int scrollState) {
1613        if (mScrollState == scrollState) {
1614            return;
1615        }
1616        mScrollState = scrollState;
1617        if (mOnScrollListener != null) {
1618            mOnScrollListener.onScrollStateChange(this, scrollState);
1619        }
1620    }
1621
1622    /**
1623     * Flings the selector with the given <code>velocityY</code>.
1624     */
1625    private void fling(int velocityY) {
1626        mPreviousScrollerY = 0;
1627
1628        if (velocityY > 0) {
1629            mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1630        } else {
1631            mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1632        }
1633
1634        invalidate();
1635    }
1636
1637    /**
1638     * @return The wrapped index <code>selectorIndex</code> value.
1639     */
1640    private int getWrappedSelectorIndex(int selectorIndex) {
1641        if (selectorIndex > mMaxValue) {
1642            return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
1643        } else if (selectorIndex < mMinValue) {
1644            return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
1645        }
1646        return selectorIndex;
1647    }
1648
1649    /**
1650     * Increments the <code>selectorIndices</code> whose string representations
1651     * will be displayed in the selector.
1652     */
1653    private void incrementSelectorIndices(int[] selectorIndices) {
1654        for (int i = 0; i < selectorIndices.length - 1; i++) {
1655            selectorIndices[i] = selectorIndices[i + 1];
1656        }
1657        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1658        if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
1659            nextScrollSelectorIndex = mMinValue;
1660        }
1661        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1662        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1663    }
1664
1665    /**
1666     * Decrements the <code>selectorIndices</code> whose string representations
1667     * will be displayed in the selector.
1668     */
1669    private void decrementSelectorIndices(int[] selectorIndices) {
1670        for (int i = selectorIndices.length - 1; i > 0; i--) {
1671            selectorIndices[i] = selectorIndices[i - 1];
1672        }
1673        int nextScrollSelectorIndex = selectorIndices[1] - 1;
1674        if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
1675            nextScrollSelectorIndex = mMaxValue;
1676        }
1677        selectorIndices[0] = nextScrollSelectorIndex;
1678        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1679    }
1680
1681    /**
1682     * Ensures we have a cached string representation of the given <code>
1683     * selectorIndex</code> 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            setValueInternal(current, true);
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     * @return Whether the text was updated.
1727     */
1728    private boolean updateInputTextView() {
1729        /*
1730         * If we don't have displayed values then use the current number else
1731         * find the correct value in the displayed values for the current
1732         * number.
1733         */
1734        String text = (mDisplayedValues == null) ? formatNumber(mValue)
1735                : mDisplayedValues[mValue - mMinValue];
1736        if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) {
1737            mInputText.setText(text);
1738            return true;
1739        }
1740
1741        return false;
1742    }
1743
1744    /**
1745     * Notifies the listener, if registered, of a change of the value of this
1746     * NumberPicker.
1747     */
1748    private void notifyChange(int previous, int current) {
1749        if (mOnValueChangeListener != null) {
1750            mOnValueChangeListener.onValueChange(this, previous, mValue);
1751        }
1752    }
1753
1754    /**
1755     * Posts a command for changing the current value by one.
1756     *
1757     * @param increment Whether to increment or decrement the value.
1758     */
1759    private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
1760        if (mChangeCurrentByOneFromLongPressCommand == null) {
1761            mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
1762        } else {
1763            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1764        }
1765        mChangeCurrentByOneFromLongPressCommand.setStep(increment);
1766        postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
1767    }
1768
1769    /**
1770     * Removes the command for changing the current value by one.
1771     */
1772    private void removeChangeCurrentByOneFromLongPress() {
1773        if (mChangeCurrentByOneFromLongPressCommand != null) {
1774            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1775        }
1776    }
1777
1778    /**
1779     * Posts a command for beginning an edit of the current value via IME on
1780     * long press.
1781     */
1782    private void postBeginSoftInputOnLongPressCommand() {
1783        if (mBeginSoftInputOnLongPressCommand == null) {
1784            mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
1785        } else {
1786            removeCallbacks(mBeginSoftInputOnLongPressCommand);
1787        }
1788        postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout());
1789    }
1790
1791    /**
1792     * Removes the command for beginning an edit of the current value via IME.
1793     */
1794    private void removeBeginSoftInputCommand() {
1795        if (mBeginSoftInputOnLongPressCommand != null) {
1796            removeCallbacks(mBeginSoftInputOnLongPressCommand);
1797        }
1798    }
1799
1800    /**
1801     * Removes all pending callback from the message queue.
1802     */
1803    private void removeAllCallbacks() {
1804        if (mChangeCurrentByOneFromLongPressCommand != null) {
1805            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1806        }
1807        if (mSetSelectionCommand != null) {
1808            removeCallbacks(mSetSelectionCommand);
1809        }
1810        if (mBeginSoftInputOnLongPressCommand != null) {
1811            removeCallbacks(mBeginSoftInputOnLongPressCommand);
1812        }
1813        mPressedStateHelper.cancel();
1814    }
1815
1816    /**
1817     * @return The selected index given its displayed <code>value</code>.
1818     */
1819    private int getSelectedPos(String value) {
1820        if (mDisplayedValues == null) {
1821            try {
1822                return Integer.parseInt(value);
1823            } catch (NumberFormatException e) {
1824                // Ignore as if it's not a number we don't care
1825            }
1826        } else {
1827            for (int i = 0; i < mDisplayedValues.length; i++) {
1828                // Don't force the user to type in jan when ja will do
1829                value = value.toLowerCase();
1830                if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1831                    return mMinValue + i;
1832                }
1833            }
1834
1835            /*
1836             * The user might have typed in a number into the month field i.e.
1837             * 10 instead of OCT so support that too.
1838             */
1839            try {
1840                return Integer.parseInt(value);
1841            } catch (NumberFormatException e) {
1842
1843                // Ignore as if it's not a number we don't care
1844            }
1845        }
1846        return mMinValue;
1847    }
1848
1849    /**
1850     * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1851     * </code> to <code>selectionEnd</code>.
1852     */
1853    private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1854        if (mSetSelectionCommand == null) {
1855            mSetSelectionCommand = new SetSelectionCommand();
1856        } else {
1857            removeCallbacks(mSetSelectionCommand);
1858        }
1859        mSetSelectionCommand.mSelectionStart = selectionStart;
1860        mSetSelectionCommand.mSelectionEnd = selectionEnd;
1861        post(mSetSelectionCommand);
1862    }
1863
1864    /**
1865     * Filter for accepting only valid indices or prefixes of the string
1866     * representation of valid indices.
1867     */
1868    class InputTextFilter extends NumberKeyListener {
1869
1870        // XXX This doesn't allow for range limits when controlled by a
1871        // soft input method!
1872        public int getInputType() {
1873            return InputType.TYPE_CLASS_TEXT;
1874        }
1875
1876        @Override
1877        protected char[] getAcceptedChars() {
1878            return DIGIT_CHARACTERS;
1879        }
1880
1881        @Override
1882        public CharSequence filter(
1883                CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
1884            if (mDisplayedValues == null) {
1885                CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1886                if (filtered == null) {
1887                    filtered = source.subSequence(start, end);
1888                }
1889
1890                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1891                        + dest.subSequence(dend, dest.length());
1892
1893                if ("".equals(result)) {
1894                    return result;
1895                }
1896                int val = getSelectedPos(result);
1897
1898                /*
1899                 * Ensure the user can't type in a value greater than the max
1900                 * allowed. We have to allow less than min as the user might
1901                 * want to delete some numbers and then type a new number.
1902                 */
1903                if (val > mMaxValue) {
1904                    return "";
1905                } else {
1906                    return filtered;
1907                }
1908            } else {
1909                CharSequence filtered = String.valueOf(source.subSequence(start, end));
1910                if (TextUtils.isEmpty(filtered)) {
1911                    return "";
1912                }
1913                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1914                        + dest.subSequence(dend, dest.length());
1915                String str = String.valueOf(result).toLowerCase();
1916                for (String val : mDisplayedValues) {
1917                    String valLowerCase = val.toLowerCase();
1918                    if (valLowerCase.startsWith(str)) {
1919                        postSetSelectionCommand(result.length(), val.length());
1920                        return val.subSequence(dstart, val.length());
1921                    }
1922                }
1923                return "";
1924            }
1925        }
1926    }
1927
1928    /**
1929     * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
1930     * middle element is in the middle of the widget.
1931     *
1932     * @return Whether an adjustment has been made.
1933     */
1934    private boolean ensureScrollWheelAdjusted() {
1935        // adjust to the closest value
1936        int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
1937        if (deltaY != 0) {
1938            mPreviousScrollerY = 0;
1939            if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
1940                deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
1941            }
1942            mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
1943            invalidate();
1944            return true;
1945        }
1946        return false;
1947    }
1948
1949    class PressedStateHelper implements Runnable {
1950        public static final int BUTTON_INCREMENT = 1;
1951        public static final int BUTTON_DECREMENT = 2;
1952
1953        private final int MODE_PRESS = 1;
1954        private final int MODE_TAPPED = 2;
1955
1956        private int mManagedButton;
1957        private int mMode;
1958
1959        public void cancel() {
1960            mMode = 0;
1961            mManagedButton = 0;
1962            NumberPicker.this.removeCallbacks(this);
1963            if (mIncrementVirtualButtonPressed) {
1964                mIncrementVirtualButtonPressed = false;
1965                invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
1966            }
1967            mDecrementVirtualButtonPressed = false;
1968            if (mDecrementVirtualButtonPressed) {
1969                invalidate(0, 0, mRight, mTopSelectionDividerTop);
1970            }
1971        }
1972
1973        public void buttonPressDelayed(int button) {
1974            cancel();
1975            mMode = MODE_PRESS;
1976            mManagedButton = button;
1977            NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
1978        }
1979
1980        public void buttonTapped(int button) {
1981            cancel();
1982            mMode = MODE_TAPPED;
1983            mManagedButton = button;
1984            NumberPicker.this.post(this);
1985        }
1986
1987        @Override
1988        public void run() {
1989            switch (mMode) {
1990                case MODE_PRESS: {
1991                    switch (mManagedButton) {
1992                        case BUTTON_INCREMENT: {
1993                            mIncrementVirtualButtonPressed = true;
1994                            invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
1995                        } break;
1996                        case BUTTON_DECREMENT: {
1997                            mDecrementVirtualButtonPressed = true;
1998                            invalidate(0, 0, mRight, mTopSelectionDividerTop);
1999                        }
2000                    }
2001                } break;
2002                case MODE_TAPPED: {
2003                    switch (mManagedButton) {
2004                        case BUTTON_INCREMENT: {
2005                            if (!mIncrementVirtualButtonPressed) {
2006                                NumberPicker.this.postDelayed(this,
2007                                        ViewConfiguration.getPressedStateDuration());
2008                            }
2009                            mIncrementVirtualButtonPressed ^= true;
2010                            invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2011                        } break;
2012                        case BUTTON_DECREMENT: {
2013                            if (!mDecrementVirtualButtonPressed) {
2014                                NumberPicker.this.postDelayed(this,
2015                                        ViewConfiguration.getPressedStateDuration());
2016                            }
2017                            mDecrementVirtualButtonPressed ^= true;
2018                            invalidate(0, 0, mRight, mTopSelectionDividerTop);
2019                        }
2020                    }
2021                } break;
2022            }
2023        }
2024    }
2025
2026    /**
2027     * Command for setting the input text selection.
2028     */
2029    class SetSelectionCommand implements Runnable {
2030        private int mSelectionStart;
2031
2032        private int mSelectionEnd;
2033
2034        public void run() {
2035            mInputText.setSelection(mSelectionStart, mSelectionEnd);
2036        }
2037    }
2038
2039    /**
2040     * Command for changing the current value from a long press by one.
2041     */
2042    class ChangeCurrentByOneFromLongPressCommand implements Runnable {
2043        private boolean mIncrement;
2044
2045        private void setStep(boolean increment) {
2046            mIncrement = increment;
2047        }
2048
2049        @Override
2050        public void run() {
2051            changeValueByOne(mIncrement);
2052            postDelayed(this, mLongPressUpdateInterval);
2053        }
2054    }
2055
2056    /**
2057     * @hide
2058     */
2059    public static class CustomEditText extends EditText {
2060
2061        public CustomEditText(Context context, AttributeSet attrs) {
2062            super(context, attrs);
2063        }
2064
2065        @Override
2066        public void onEditorAction(int actionCode) {
2067            super.onEditorAction(actionCode);
2068            if (actionCode == EditorInfo.IME_ACTION_DONE) {
2069                clearFocus();
2070            }
2071        }
2072    }
2073
2074    /**
2075     * Command for beginning soft input on long press.
2076     */
2077    class BeginSoftInputOnLongPressCommand implements Runnable {
2078
2079        @Override
2080        public void run() {
2081            showSoftInput();
2082            mIngonreMoveEvents = true;
2083        }
2084    }
2085
2086    /**
2087     * Class for managing virtual view tree rooted at this picker.
2088     */
2089    class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
2090        private static final int UNDEFINED = Integer.MIN_VALUE;
2091
2092        private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
2093
2094        private static final int VIRTUAL_VIEW_ID_INPUT = 2;
2095
2096        private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
2097
2098        private final Rect mTempRect = new Rect();
2099
2100        private final int[] mTempArray = new int[2];
2101
2102        private int mAccessibilityFocusedView = UNDEFINED;
2103
2104        @Override
2105        public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
2106            switch (virtualViewId) {
2107                case View.NO_ID:
2108                    return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
2109                            mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
2110                case VIRTUAL_VIEW_ID_DECREMENT:
2111                    return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
2112                            getVirtualDecrementButtonText(), mScrollX, mScrollY,
2113                            mScrollX + (mRight - mLeft),
2114                            mTopSelectionDividerTop + mSelectionDividerHeight);
2115                case VIRTUAL_VIEW_ID_INPUT:
2116                    return createAccessibiltyNodeInfoForInputText();
2117                case VIRTUAL_VIEW_ID_INCREMENT:
2118                    return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
2119                            getVirtualIncrementButtonText(), mScrollX,
2120                            mBottomSelectionDividerBottom - mSelectionDividerHeight,
2121                            mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
2122            }
2123            return super.createAccessibilityNodeInfo(virtualViewId);
2124        }
2125
2126        @Override
2127        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
2128                int virtualViewId) {
2129            if (TextUtils.isEmpty(searched)) {
2130                return Collections.emptyList();
2131            }
2132            String searchedLowerCase = searched.toLowerCase();
2133            List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
2134            switch (virtualViewId) {
2135                case View.NO_ID: {
2136                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2137                            VIRTUAL_VIEW_ID_DECREMENT, result);
2138                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2139                            VIRTUAL_VIEW_ID_INPUT, result);
2140                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2141                            VIRTUAL_VIEW_ID_INCREMENT, result);
2142                    return result;
2143                }
2144                case VIRTUAL_VIEW_ID_DECREMENT:
2145                case VIRTUAL_VIEW_ID_INCREMENT:
2146                case VIRTUAL_VIEW_ID_INPUT: {
2147                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId,
2148                            result);
2149                    return result;
2150                }
2151            }
2152            return super.findAccessibilityNodeInfosByText(searched, virtualViewId);
2153        }
2154
2155        @Override
2156        public boolean performAction(int virtualViewId, int action, Bundle arguments) {
2157            switch (virtualViewId) {
2158                case View.NO_ID: {
2159                    switch (action) {
2160                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2161                            if (mAccessibilityFocusedView != virtualViewId) {
2162                                mAccessibilityFocusedView = virtualViewId;
2163                                requestAccessibilityFocus();
2164                                return true;
2165                            }
2166                        } return false;
2167                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2168                            if (mAccessibilityFocusedView == virtualViewId) {
2169                                mAccessibilityFocusedView = UNDEFINED;
2170                                clearAccessibilityFocus();
2171                                return true;
2172                            }
2173                            return false;
2174                        }
2175                        case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
2176                            if (NumberPicker.this.isEnabled()
2177                                    && (getWrapSelectorWheel() || getValue() < getMaxValue())) {
2178                                changeValueByOne(true);
2179                                return true;
2180                            }
2181                        } return false;
2182                        case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
2183                            if (NumberPicker.this.isEnabled()
2184                                    && (getWrapSelectorWheel() || getValue() > getMinValue())) {
2185                                changeValueByOne(false);
2186                                return true;
2187                            }
2188                        } return false;
2189                    }
2190                } break;
2191                case VIRTUAL_VIEW_ID_INPUT: {
2192                    switch (action) {
2193                        case AccessibilityNodeInfo.ACTION_FOCUS: {
2194                            if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) {
2195                                return mInputText.requestFocus();
2196                            }
2197                        } break;
2198                        case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
2199                            if (NumberPicker.this.isEnabled() && mInputText.isFocused()) {
2200                                mInputText.clearFocus();
2201                                return true;
2202                            }
2203                            return false;
2204                        }
2205                        case AccessibilityNodeInfo.ACTION_CLICK: {
2206                            if (NumberPicker.this.isEnabled()) {
2207                                showSoftInput();
2208                                return true;
2209                            }
2210                            return false;
2211                        }
2212                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2213                            if (mAccessibilityFocusedView != virtualViewId) {
2214                                mAccessibilityFocusedView = virtualViewId;
2215                                sendAccessibilityEventForVirtualView(virtualViewId,
2216                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2217                                mInputText.invalidate();
2218                                return true;
2219                            }
2220                        } return false;
2221                        case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2222                            if (mAccessibilityFocusedView == virtualViewId) {
2223                                mAccessibilityFocusedView = UNDEFINED;
2224                                sendAccessibilityEventForVirtualView(virtualViewId,
2225                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2226                                mInputText.invalidate();
2227                                return true;
2228                            }
2229                        } return false;
2230                        default: {
2231                            return mInputText.performAccessibilityAction(action, arguments);
2232                        }
2233                    }
2234                } return false;
2235                case VIRTUAL_VIEW_ID_INCREMENT: {
2236                    switch (action) {
2237                        case AccessibilityNodeInfo.ACTION_CLICK: {
2238                            if (NumberPicker.this.isEnabled()) {
2239                                NumberPicker.this.changeValueByOne(true);
2240                                sendAccessibilityEventForVirtualView(virtualViewId,
2241                                        AccessibilityEvent.TYPE_VIEW_CLICKED);
2242                                return true;
2243                            }
2244                        } return false;
2245                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2246                            if (mAccessibilityFocusedView != virtualViewId) {
2247                                mAccessibilityFocusedView = virtualViewId;
2248                                sendAccessibilityEventForVirtualView(virtualViewId,
2249                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2250                                invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2251                                return true;
2252                            }
2253                        } return false;
2254                        case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2255                            if (mAccessibilityFocusedView == virtualViewId) {
2256                                mAccessibilityFocusedView = UNDEFINED;
2257                                sendAccessibilityEventForVirtualView(virtualViewId,
2258                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2259                                invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2260                                return true;
2261                            }
2262                        } return false;
2263                    }
2264                } return false;
2265                case VIRTUAL_VIEW_ID_DECREMENT: {
2266                    switch (action) {
2267                        case AccessibilityNodeInfo.ACTION_CLICK: {
2268                            if (NumberPicker.this.isEnabled()) {
2269                                final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
2270                                NumberPicker.this.changeValueByOne(increment);
2271                                sendAccessibilityEventForVirtualView(virtualViewId,
2272                                        AccessibilityEvent.TYPE_VIEW_CLICKED);
2273                                return true;
2274                            }
2275                        } return false;
2276                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2277                            if (mAccessibilityFocusedView != virtualViewId) {
2278                                mAccessibilityFocusedView = virtualViewId;
2279                                sendAccessibilityEventForVirtualView(virtualViewId,
2280                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2281                                invalidate(0, 0, mRight, mTopSelectionDividerTop);
2282                                return true;
2283                            }
2284                        } return false;
2285                        case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2286                            if (mAccessibilityFocusedView == virtualViewId) {
2287                                mAccessibilityFocusedView = UNDEFINED;
2288                                sendAccessibilityEventForVirtualView(virtualViewId,
2289                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2290                                invalidate(0, 0, mRight, mTopSelectionDividerTop);
2291                                return true;
2292                            }
2293                        } return false;
2294                    }
2295                } return false;
2296            }
2297            return super.performAction(virtualViewId, action, arguments);
2298        }
2299
2300        @Override
2301        public AccessibilityNodeInfo findAccessibilityFocus(int virtualViewId) {
2302            return createAccessibilityNodeInfo(mAccessibilityFocusedView);
2303        }
2304
2305        @Override
2306        public AccessibilityNodeInfo accessibilityFocusSearch(int direction, int virtualViewId) {
2307            switch (direction) {
2308                case View.ACCESSIBILITY_FOCUS_DOWN:
2309                case View.ACCESSIBILITY_FOCUS_FORWARD: {
2310                    switch (mAccessibilityFocusedView) {
2311                        case UNDEFINED: {
2312                            return createAccessibilityNodeInfo(View.NO_ID);
2313                        }
2314                        case View.NO_ID: {
2315                            if (hasVirtualDecrementButton()) {
2316                                return createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT);
2317                            }
2318                        }
2319                        //$FALL-THROUGH$
2320                        case VIRTUAL_VIEW_ID_DECREMENT: {
2321                            return createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT);
2322                        }
2323                        case VIRTUAL_VIEW_ID_INPUT: {
2324                            if (hasVirtualIncrementButton()) {
2325                                return createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT);
2326                            }
2327                        }
2328                        //$FALL-THROUGH$
2329                        case VIRTUAL_VIEW_ID_INCREMENT: {
2330                            View nextFocus = NumberPicker.this.focusSearch(direction);
2331                            if (nextFocus != null) {
2332                                return nextFocus.createAccessibilityNodeInfo();
2333                            }
2334                            return null;
2335                        }
2336                    }
2337                } break;
2338                case View.ACCESSIBILITY_FOCUS_UP:
2339                case View.ACCESSIBILITY_FOCUS_BACKWARD: {
2340                    switch (mAccessibilityFocusedView) {
2341                        case UNDEFINED: {
2342                            return createAccessibilityNodeInfo(View.NO_ID);
2343                        }
2344                        case View.NO_ID: {
2345                            if (hasVirtualIncrementButton()) {
2346                                return createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT);
2347                            }
2348                        }
2349                        //$FALL-THROUGH$
2350                        case VIRTUAL_VIEW_ID_INCREMENT: {
2351                            return createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT);
2352                        }
2353                        case VIRTUAL_VIEW_ID_INPUT: {
2354                            if (hasVirtualDecrementButton()) {
2355                                return createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT);
2356                            }
2357                        }
2358                        //$FALL-THROUGH$
2359                        case VIRTUAL_VIEW_ID_DECREMENT: {
2360                            View nextFocus = NumberPicker.this.focusSearch(direction);
2361                            if (nextFocus != null) {
2362                                return nextFocus.createAccessibilityNodeInfo();
2363                            }
2364                            return null;
2365                        }
2366                    }
2367                } break;
2368            }
2369            return null;
2370        }
2371
2372        public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) {
2373            switch (virtualViewId) {
2374                case VIRTUAL_VIEW_ID_DECREMENT: {
2375                    if (hasVirtualDecrementButton()) {
2376                        sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
2377                                getVirtualDecrementButtonText());
2378                    }
2379                } break;
2380                case VIRTUAL_VIEW_ID_INPUT: {
2381                    sendAccessibilityEventForVirtualText(eventType);
2382                } break;
2383                case VIRTUAL_VIEW_ID_INCREMENT: {
2384                    if (hasVirtualIncrementButton()) {
2385                        sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
2386                                getVirtualIncrementButtonText());
2387                    }
2388                } break;
2389            }
2390        }
2391
2392        private void sendAccessibilityEventForVirtualText(int eventType) {
2393            AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
2394            mInputText.onInitializeAccessibilityEvent(event);
2395            mInputText.onPopulateAccessibilityEvent(event);
2396            event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
2397            requestSendAccessibilityEvent(NumberPicker.this, event);
2398        }
2399
2400        private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
2401                String text) {
2402            AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
2403            event.setClassName(Button.class.getName());
2404            event.setPackageName(mContext.getPackageName());
2405            event.getText().add(text);
2406            event.setEnabled(NumberPicker.this.isEnabled());
2407            event.setSource(NumberPicker.this, virtualViewId);
2408            requestSendAccessibilityEvent(NumberPicker.this, event);
2409        }
2410
2411        private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
2412                int virtualViewId, List<AccessibilityNodeInfo> outResult) {
2413            switch (virtualViewId) {
2414                case VIRTUAL_VIEW_ID_DECREMENT: {
2415                    String text = getVirtualDecrementButtonText();
2416                    if (!TextUtils.isEmpty(text)
2417                            && text.toString().toLowerCase().contains(searchedLowerCase)) {
2418                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
2419                    }
2420                } return;
2421                case VIRTUAL_VIEW_ID_INPUT: {
2422                    CharSequence text = mInputText.getText();
2423                    if (!TextUtils.isEmpty(text) &&
2424                            text.toString().toLowerCase().contains(searchedLowerCase)) {
2425                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
2426                        return;
2427                    }
2428                    CharSequence contentDesc = mInputText.getText();
2429                    if (!TextUtils.isEmpty(contentDesc) &&
2430                            contentDesc.toString().toLowerCase().contains(searchedLowerCase)) {
2431                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
2432                        return;
2433                    }
2434                } break;
2435                case VIRTUAL_VIEW_ID_INCREMENT: {
2436                    String text = getVirtualIncrementButtonText();
2437                    if (!TextUtils.isEmpty(text)
2438                            && text.toString().toLowerCase().contains(searchedLowerCase)) {
2439                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
2440                    }
2441                } return;
2442            }
2443        }
2444
2445        private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText() {
2446            AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
2447            info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
2448            if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
2449                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2450            }
2451            if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
2452                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2453            }
2454            return info;
2455        }
2456
2457        private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId,
2458                String text, int left, int top, int right, int bottom) {
2459            AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
2460            info.setClassName(Button.class.getName());
2461            info.setPackageName(mContext.getPackageName());
2462            info.setSource(NumberPicker.this, virtualViewId);
2463            info.setParent(NumberPicker.this);
2464            info.setText(text);
2465            info.setClickable(true);
2466            info.setLongClickable(true);
2467            info.setEnabled(NumberPicker.this.isEnabled());
2468            Rect boundsInParent = mTempRect;
2469            boundsInParent.set(left, top, right, bottom);
2470            info.setVisibleToUser(isVisibleToUser(boundsInParent));
2471            info.setBoundsInParent(boundsInParent);
2472            Rect boundsInScreen = boundsInParent;
2473            int[] locationOnScreen = mTempArray;
2474            getLocationOnScreen(locationOnScreen);
2475            boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2476            info.setBoundsInScreen(boundsInScreen);
2477
2478            if (mAccessibilityFocusedView != virtualViewId) {
2479                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2480            }
2481            if (mAccessibilityFocusedView == virtualViewId) {
2482                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2483            }
2484            if (NumberPicker.this.isEnabled()) {
2485                info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
2486            }
2487
2488            return info;
2489        }
2490
2491        private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
2492                int right, int bottom) {
2493            AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
2494            info.setClassName(NumberPicker.class.getName());
2495            info.setPackageName(mContext.getPackageName());
2496            info.setSource(NumberPicker.this);
2497
2498            if (hasVirtualDecrementButton()) {
2499                info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
2500            }
2501            info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
2502            if (hasVirtualIncrementButton()) {
2503                info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
2504            }
2505
2506            info.setParent((View) getParent());
2507            info.setEnabled(NumberPicker.this.isEnabled());
2508            info.setScrollable(true);
2509            Rect boundsInParent = mTempRect;
2510            boundsInParent.set(left, top, right, bottom);
2511            info.setBoundsInParent(boundsInParent);
2512            info.setVisibleToUser(isVisibleToUser());
2513            Rect boundsInScreen = boundsInParent;
2514            int[] locationOnScreen = mTempArray;
2515            getLocationOnScreen(locationOnScreen);
2516            boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2517            info.setBoundsInScreen(boundsInScreen);
2518
2519            if (mAccessibilityFocusedView != View.NO_ID) {
2520                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2521            }
2522            if (mAccessibilityFocusedView == View.NO_ID) {
2523                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2524            }
2525            if (NumberPicker.this.isEnabled()) {
2526                if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
2527                    info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
2528                }
2529                if (getWrapSelectorWheel() || getValue() > getMinValue()) {
2530                    info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
2531                }
2532            }
2533
2534            return info;
2535        }
2536
2537        private boolean hasVirtualDecrementButton() {
2538            return getWrapSelectorWheel() || getValue() > getMinValue();
2539        }
2540
2541        private boolean hasVirtualIncrementButton() {
2542            return getWrapSelectorWheel() || getValue() < getMaxValue();
2543        }
2544
2545        private String getVirtualDecrementButtonText() {
2546            int value = mValue - 1;
2547            if (mWrapSelectorWheel) {
2548                value = getWrappedSelectorIndex(value);
2549            }
2550            if (value >= mMinValue) {
2551                return (mDisplayedValues == null) ? formatNumber(value)
2552                        : mDisplayedValues[value - mMinValue];
2553            }
2554            return null;
2555        }
2556
2557        private String getVirtualIncrementButtonText() {
2558            int value = mValue + 1;
2559            if (mWrapSelectorWheel) {
2560                value = getWrappedSelectorIndex(value);
2561            }
2562            if (value <= mMaxValue) {
2563                return (mDisplayedValues == null) ? formatNumber(value)
2564                        : mDisplayedValues[value - mMinValue];
2565            }
2566            return null;
2567        }
2568    }
2569}
2570