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