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