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