• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.annotation.Widget;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Align;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.text.InputFilter;
31 import android.text.InputType;
32 import android.text.Spanned;
33 import android.text.TextUtils;
34 import android.text.method.NumberKeyListener;
35 import android.util.AttributeSet;
36 import android.util.SparseArray;
37 import android.util.TypedValue;
38 import android.view.KeyEvent;
39 import android.view.LayoutInflater;
40 import android.view.LayoutInflater.Filter;
41 import android.view.MotionEvent;
42 import android.view.VelocityTracker;
43 import android.view.View;
44 import android.view.ViewConfiguration;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityManager;
47 import android.view.accessibility.AccessibilityNodeInfo;
48 import android.view.accessibility.AccessibilityNodeProvider;
49 import android.view.animation.DecelerateInterpolator;
50 import android.view.inputmethod.EditorInfo;
51 import android.view.inputmethod.InputMethodManager;
52 
53 import com.android.internal.R;
54 import libcore.icu.LocaleData;
55 
56 import java.util.ArrayList;
57 import java.util.Collections;
58 import java.util.List;
59 import 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
90 public 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 
TwoDigitFormatter()161         TwoDigitFormatter() {
162             final Locale locale = Locale.getDefault();
163             init(locale);
164         }
165 
init(Locale locale)166         private void init(Locale locale) {
167             mFmt = createFormatter(locale);
168             mZeroDigit = getZeroDigit(locale);
169         }
170 
format(int value)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 
getZeroDigit(Locale locale)182         private static char getZeroDigit(Locale locale) {
183             return LocaleData.get(locale).zeroDigit;
184         }
185 
createFormatter(Locale locale)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      */
getTwoDigitFormatter()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          */
onValueChange(NumberPicker picker, int oldVal, int newVal)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          */
onScrollStateChange(NumberPicker view, int scrollState)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          */
format(int value)530         public String format(int value);
531     }
532 
533     /**
534      * Create a new number picker.
535      *
536      * @param context The application environment.
537      */
NumberPicker(Context context)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      */
NumberPicker(Context context, AttributeSet attrs)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      */
NumberPicker(Context context, AttributeSet attrs, int defStyle)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
onLayout(boolean changed, int left, int top, int right, int bottom)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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      */
moveToFinalScrollerPosition(Scroller scroller)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
onInterceptTouchEvent(MotionEvent event)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
onTouchEvent(MotionEvent event)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
dispatchTouchEvent(MotionEvent event)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
dispatchKeyEvent(KeyEvent event)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
dispatchTrackballEvent(MotionEvent event)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
dispatchHoverEvent(MotionEvent event)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
computeScroll()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
setEnabled(boolean enabled)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
scrollBy(int x, int y)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
getSolidColor()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      */
setOnValueChangedListener(OnValueChangeListener onValueChangedListener)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      */
setOnScrollListener(OnScrollListener onScrollListener)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      */
setFormatter(Formatter formatter)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      */
setValue(int value)1142     public void setValue(int value) {
1143         setValueInternal(value, false);
1144     }
1145 
1146     /**
1147      * Shows the soft input for its input text.
1148      */
showSoftInput()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      */
hideSoftInput()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      */
tryComputeMaxWidth()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      */
getWrapSelectorWheel()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      */
setWrapSelectorWheel(boolean wrapSelectorWheel)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      */
setOnLongPressUpdateInterval(long intervalMillis)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      */
getValue()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      */
getMinValue()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      */
setMinValue(int minValue)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      */
getMaxValue()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      */
setMaxValue(int maxValue)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      */
getDisplayedValues()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      */
setDisplayedValues(String[] displayedValues)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
getTopFadingEdgeStrength()1387     protected float getTopFadingEdgeStrength() {
1388         return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1389     }
1390 
1391     @Override
getBottomFadingEdgeStrength()1392     protected float getBottomFadingEdgeStrength() {
1393         return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1394     }
1395 
1396     @Override
onDetachedFromWindow()1397     protected void onDetachedFromWindow() {
1398         removeAllCallbacks();
1399     }
1400 
1401     @Override
onDraw(Canvas canvas)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
onInitializeAccessibilityEvent(AccessibilityEvent event)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
getAccessibilityNodeProvider()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      */
makeMeasureSpec(int measureSpec, int maxSize)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      */
resolveSizeAndStateRespectingMinSize( int minSize, int measuredSize, int measureSpec)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      */
initializeSelectorWheelIndices()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      */
setValueInternal(int current, boolean notifyChange)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      */
changeValueByOne(boolean increment)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 
initializeSelectorWheel()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 
initializeFadingEdges()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      */
onScrollerFinished(Scroller scroller)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      */
onScrollStateChange(int scrollState)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      */
fling(int velocityY)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      */
getWrappedSelectorIndex(int selectorIndex)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      */
incrementSelectorIndices(int[] selectorIndices)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      */
decrementSelectorIndices(int[] selectorIndices)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      */
ensureCachedScrollSelectorValue(int selectorIndex)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 
formatNumber(int value)1730     private String formatNumber(int value) {
1731         return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
1732     }
1733 
validateInputTextView(View v)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      */
updateInputTextView()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      */
notifyChange(int previous, int current)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      */
postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis)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      */
removeChangeCurrentByOneFromLongPress()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      */
postBeginSoftInputOnLongPressCommand()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      */
removeBeginSoftInputCommand()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      */
removeAllCallbacks()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      */
getSelectedPos(String value)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      */
postSetSelectionCommand(int selectionStart, int selectionEnd)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!
getInputType()1912         public int getInputType() {
1913             return InputType.TYPE_CLASS_TEXT;
1914         }
1915 
1916         @Override
getAcceptedChars()1917         protected char[] getAcceptedChars() {
1918             return DIGIT_CHARACTERS;
1919         }
1920 
1921         @Override
filter( CharSequence source, int start, int end, Spanned dest, int dstart, int dend)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      */
ensureScrollWheelAdjusted()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 
cancel()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 
buttonPressDelayed(int button)2013         public void buttonPressDelayed(int button) {
2014             cancel();
2015             mMode = MODE_PRESS;
2016             mManagedButton = button;
2017             NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
2018         }
2019 
buttonTapped(int button)2020         public void buttonTapped(int button) {
2021             cancel();
2022             mMode = MODE_TAPPED;
2023             mManagedButton = button;
2024             NumberPicker.this.post(this);
2025         }
2026 
2027         @Override
run()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 
run()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 
setStep(boolean increment)2085         private void setStep(boolean increment) {
2086             mIncrement = increment;
2087         }
2088 
2089         @Override
run()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 
CustomEditText(Context context, AttributeSet attrs)2101         public CustomEditText(Context context, AttributeSet attrs) {
2102             super(context, attrs);
2103         }
2104 
2105         @Override
onEditorAction(int actionCode)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
run()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
createAccessibilityNodeInfo(int virtualViewId)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
findAccessibilityNodeInfosByText(String searched, int virtualViewId)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
performAction(int virtualViewId, int action, Bundle arguments)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 
sendAccessibilityEventForVirtualView(int virtualViewId, int eventType)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 
sendAccessibilityEventForVirtualText(int eventType)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 
sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, String text)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 
findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, int virtualViewId, List<AccessibilityNodeInfo> outResult)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 
createAccessibiltyNodeInfoForInputText()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 
createAccessibilityNodeInfoForVirtualButton(int virtualViewId, String text, int left, int top, int right, int bottom)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 
createAccessibilityNodeInfoForNumberPicker(int left, int top, int right, int bottom)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 
hasVirtualDecrementButton()2517         private boolean hasVirtualDecrementButton() {
2518             return getWrapSelectorWheel() || getValue() > getMinValue();
2519         }
2520 
hasVirtualIncrementButton()2521         private boolean hasVirtualIncrementButton() {
2522             return getWrapSelectorWheel() || getValue() < getMaxValue();
2523         }
2524 
getVirtualDecrementButtonText()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 
getVirtualIncrementButtonText()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 
formatNumberWithLocale(int value)2550     static private String formatNumberWithLocale(int value) {
2551         return String.format(Locale.getDefault(), "%d", value);
2552     }
2553 }
2554