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