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