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