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