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