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