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