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