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