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