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