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