1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.annotation.CallSuper;
20 import android.annotation.ColorInt;
21 import android.annotation.FloatRange;
22 import android.annotation.IntDef;
23 import android.annotation.IntRange;
24 import android.annotation.Px;
25 import android.annotation.TestApi;
26 import android.annotation.Widget;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.Context;
29 import android.content.res.ColorStateList;
30 import android.content.res.TypedArray;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.Paint;
34 import android.graphics.Paint.Align;
35 import android.graphics.Rect;
36 import android.graphics.drawable.Drawable;
37 import android.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 
778         mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
779         mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
780 
781         // initialize constants
782         ViewConfiguration configuration = ViewConfiguration.get(context);
783         mTouchSlop = configuration.getScaledTouchSlop();
784         mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
785         mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
786                 / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
787         mTextSize = (int) mInputText.getTextSize();
788 
789         // create the selector wheel paint
790         Paint paint = new Paint();
791         paint.setAntiAlias(true);
792         paint.setTextAlign(Align.CENTER);
793         paint.setTextSize(mTextSize);
794         paint.setTypeface(mInputText.getTypeface());
795         ColorStateList colors = mInputText.getTextColors();
796         int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
797         paint.setColor(color);
798         mSelectorWheelPaint = paint;
799 
800         // create the fling and adjust scrollers
801         mFlingScroller = new Scroller(getContext(), null, true);
802         mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
803 
804         updateInputTextView();
805 
806         // If not explicitly specified this view is important for accessibility.
807         if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
808             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
809         }
810 
811         // Should be focusable by default, as the text view whose visibility changes is focusable
812         if (getFocusable() == View.FOCUSABLE_AUTO) {
813             setFocusable(View.FOCUSABLE);
814             setFocusableInTouchMode(true);
815         }
816     }
817 
818     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)819     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
820         if (!mHasSelectorWheel) {
821             super.onLayout(changed, left, top, right, bottom);
822             return;
823         }
824         final int msrdWdth = getMeasuredWidth();
825         final int msrdHght = getMeasuredHeight();
826 
827         // Input text centered horizontally.
828         final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
829         final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
830         final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
831         final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
832         final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
833         final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
834         mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
835 
836         if (changed) {
837             // need to do all this when we know our size
838             initializeSelectorWheel();
839             initializeFadingEdges();
840             mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
841                     - mSelectionDividerHeight;
842             mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
843                     + mSelectionDividersDistance;
844         }
845     }
846 
847     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)848     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
849         if (!mHasSelectorWheel) {
850             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
851             return;
852         }
853         // Try greedily to fit the max width and height.
854         final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
855         final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
856         super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
857         // Flag if we are measured with width or height less than the respective min.
858         final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
859                 widthMeasureSpec);
860         final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
861                 heightMeasureSpec);
862         setMeasuredDimension(widthSize, heightSize);
863     }
864 
865     /**
866      * Move to the final position of a scroller. Ensures to force finish the scroller
867      * and if it is not at its final position a scroll of the selector wheel is
868      * performed to fast forward to the final position.
869      *
870      * @param scroller The scroller to whose final position to get.
871      * @return True of the a move was performed, i.e. the scroller was not in final position.
872      */
moveToFinalScrollerPosition(Scroller scroller)873     private boolean moveToFinalScrollerPosition(Scroller scroller) {
874         scroller.forceFinished(true);
875         int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
876         int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight;
877         int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
878         if (overshootAdjustment != 0) {
879             if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) {
880                 if (overshootAdjustment > 0) {
881                     overshootAdjustment -= mSelectorElementHeight;
882                 } else {
883                     overshootAdjustment += mSelectorElementHeight;
884                 }
885             }
886             amountToScroll += overshootAdjustment;
887             scrollBy(0, amountToScroll);
888             return true;
889         }
890         return false;
891     }
892 
893     @Override
onInterceptTouchEvent(MotionEvent event)894     public boolean onInterceptTouchEvent(MotionEvent event) {
895         if (!mHasSelectorWheel || !isEnabled()) {
896             return false;
897         }
898         final int action = event.getActionMasked();
899         switch (action) {
900             case MotionEvent.ACTION_DOWN: {
901                 removeAllCallbacks();
902                 hideSoftInput();
903                 mLastDownOrMoveEventY = mLastDownEventY = event.getY();
904                 mLastDownEventTime = event.getEventTime();
905                 mIgnoreMoveEvents = false;
906                 mPerformClickOnTap = false;
907                 // Handle pressed state before any state change.
908                 if (mLastDownEventY < mTopSelectionDividerTop) {
909                     if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
910                         mPressedStateHelper.buttonPressDelayed(
911                                 PressedStateHelper.BUTTON_DECREMENT);
912                     }
913                 } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
914                     if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
915                         mPressedStateHelper.buttonPressDelayed(
916                                 PressedStateHelper.BUTTON_INCREMENT);
917                     }
918                 }
919                 // Make sure we support flinging inside scrollables.
920                 getParent().requestDisallowInterceptTouchEvent(true);
921                 if (!mFlingScroller.isFinished()) {
922                     mFlingScroller.forceFinished(true);
923                     mAdjustScroller.forceFinished(true);
924                     onScrollerFinished(mFlingScroller);
925                     onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
926                 } else if (!mAdjustScroller.isFinished()) {
927                     mFlingScroller.forceFinished(true);
928                     mAdjustScroller.forceFinished(true);
929                     onScrollerFinished(mAdjustScroller);
930                 } else if (mLastDownEventY < mTopSelectionDividerTop) {
931                     postChangeCurrentByOneFromLongPress(
932                             false, ViewConfiguration.getLongPressTimeout());
933                 } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
934                     postChangeCurrentByOneFromLongPress(
935                             true, ViewConfiguration.getLongPressTimeout());
936                 } else {
937                     mPerformClickOnTap = true;
938                     postBeginSoftInputOnLongPressCommand();
939                 }
940                 return true;
941             }
942         }
943         return false;
944     }
945 
946     @Override
onTouchEvent(MotionEvent event)947     public boolean onTouchEvent(MotionEvent event) {
948         if (!isEnabled() || !mHasSelectorWheel) {
949             return false;
950         }
951         if (mVelocityTracker == null) {
952             mVelocityTracker = VelocityTracker.obtain();
953         }
954         mVelocityTracker.addMovement(event);
955         int action = event.getActionMasked();
956         switch (action) {
957             case MotionEvent.ACTION_MOVE: {
958                 if (mIgnoreMoveEvents) {
959                     break;
960                 }
961                 float currentMoveY = event.getY();
962                 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
963                     int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
964                     if (deltaDownY > mTouchSlop) {
965                         removeAllCallbacks();
966                         onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
967                     }
968                 } else {
969                     int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
970                     scrollBy(0, deltaMoveY);
971                     invalidate();
972                 }
973                 mLastDownOrMoveEventY = currentMoveY;
974             } break;
975             case MotionEvent.ACTION_UP: {
976                 removeBeginSoftInputCommand();
977                 removeChangeCurrentByOneFromLongPress();
978                 mPressedStateHelper.cancel();
979                 VelocityTracker velocityTracker = mVelocityTracker;
980                 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
981                 int initialVelocity = (int) velocityTracker.getYVelocity();
982                 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
983                     fling(initialVelocity);
984                     onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
985                 } else {
986                     int eventY = (int) event.getY();
987                     int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
988                     long deltaTime = event.getEventTime() - mLastDownEventTime;
989                     if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
990                         if (mPerformClickOnTap) {
991                             mPerformClickOnTap = false;
992                             performClick();
993                         } else {
994                             int selectorIndexOffset = (eventY / mSelectorElementHeight)
995                                     - SELECTOR_MIDDLE_ITEM_INDEX;
996                             if (selectorIndexOffset > 0) {
997                                 changeValueByOne(true);
998                                 mPressedStateHelper.buttonTapped(
999                                         PressedStateHelper.BUTTON_INCREMENT);
1000                             } else if (selectorIndexOffset < 0) {
1001                                 changeValueByOne(false);
1002                                 mPressedStateHelper.buttonTapped(
1003                                         PressedStateHelper.BUTTON_DECREMENT);
1004                             }
1005                         }
1006                     } else {
1007                         ensureScrollWheelAdjusted();
1008                     }
1009                     onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1010                 }
1011                 mVelocityTracker.recycle();
1012                 mVelocityTracker = null;
1013             } break;
1014         }
1015         return true;
1016     }
1017 
1018     @Override
dispatchTouchEvent(MotionEvent event)1019     public boolean dispatchTouchEvent(MotionEvent event) {
1020         final int action = event.getActionMasked();
1021         switch (action) {
1022             case MotionEvent.ACTION_CANCEL:
1023             case MotionEvent.ACTION_UP:
1024                 removeAllCallbacks();
1025                 break;
1026         }
1027         return super.dispatchTouchEvent(event);
1028     }
1029 
1030     @Override
dispatchKeyEvent(KeyEvent event)1031     public boolean dispatchKeyEvent(KeyEvent event) {
1032         final int keyCode = event.getKeyCode();
1033         switch (keyCode) {
1034             case KeyEvent.KEYCODE_DPAD_CENTER:
1035             case KeyEvent.KEYCODE_ENTER:
1036             case KeyEvent.KEYCODE_NUMPAD_ENTER:
1037                 removeAllCallbacks();
1038                 break;
1039             case KeyEvent.KEYCODE_DPAD_DOWN:
1040             case KeyEvent.KEYCODE_DPAD_UP:
1041                 if (!mHasSelectorWheel) {
1042                     break;
1043                 }
1044                 switch (event.getAction()) {
1045                     case KeyEvent.ACTION_DOWN:
1046                         if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
1047                                 ? getValue() < getMaxValue() : getValue() > getMinValue())) {
1048                             requestFocus();
1049                             mLastHandledDownDpadKeyCode = keyCode;
1050                             removeAllCallbacks();
1051                             if (mFlingScroller.isFinished()) {
1052                                 changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN);
1053                             }
1054                             return true;
1055                         }
1056                         break;
1057                     case KeyEvent.ACTION_UP:
1058                         if (mLastHandledDownDpadKeyCode == keyCode) {
1059                             mLastHandledDownDpadKeyCode = -1;
1060                             return true;
1061                         }
1062                         break;
1063                 }
1064         }
1065         return super.dispatchKeyEvent(event);
1066     }
1067 
1068     @Override
dispatchTrackballEvent(MotionEvent event)1069     public boolean dispatchTrackballEvent(MotionEvent event) {
1070         final int action = event.getActionMasked();
1071         switch (action) {
1072             case MotionEvent.ACTION_CANCEL:
1073             case MotionEvent.ACTION_UP:
1074                 removeAllCallbacks();
1075                 break;
1076         }
1077         return super.dispatchTrackballEvent(event);
1078     }
1079 
1080     @Override
dispatchHoverEvent(MotionEvent event)1081     protected boolean dispatchHoverEvent(MotionEvent event) {
1082         if (!mHasSelectorWheel) {
1083             return super.dispatchHoverEvent(event);
1084         }
1085         if (AccessibilityManager.getInstance(mContext).isEnabled()) {
1086             final int eventY = (int) event.getY();
1087             final int hoveredVirtualViewId;
1088             if (eventY < mTopSelectionDividerTop) {
1089                 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
1090             } else if (eventY > mBottomSelectionDividerBottom) {
1091                 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
1092             } else {
1093                 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
1094             }
1095             final int action = event.getActionMasked();
1096             AccessibilityNodeProviderImpl provider =
1097                 (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider();
1098             switch (action) {
1099                 case MotionEvent.ACTION_HOVER_ENTER: {
1100                     provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
1101                             AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
1102                     mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
1103                     provider.performAction(hoveredVirtualViewId,
1104                             AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
1105                 } break;
1106                 case MotionEvent.ACTION_HOVER_MOVE: {
1107                     if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
1108                             && mLastHoveredChildVirtualViewId != View.NO_ID) {
1109                         provider.sendAccessibilityEventForVirtualView(
1110                                 mLastHoveredChildVirtualViewId,
1111                                 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1112                         provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
1113                                 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
1114                         mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
1115                         provider.performAction(hoveredVirtualViewId,
1116                                 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
1117                     }
1118                 } break;
1119                 case MotionEvent.ACTION_HOVER_EXIT: {
1120                     provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
1121                             AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1122                     mLastHoveredChildVirtualViewId = View.NO_ID;
1123                 } break;
1124             }
1125         }
1126         return false;
1127     }
1128 
1129     @Override
computeScroll()1130     public void computeScroll() {
1131         Scroller scroller = mFlingScroller;
1132         if (scroller.isFinished()) {
1133             scroller = mAdjustScroller;
1134             if (scroller.isFinished()) {
1135                 return;
1136             }
1137         }
1138         scroller.computeScrollOffset();
1139         int currentScrollerY = scroller.getCurrY();
1140         if (mPreviousScrollerY == 0) {
1141             mPreviousScrollerY = scroller.getStartY();
1142         }
1143         scrollBy(0, currentScrollerY - mPreviousScrollerY);
1144         mPreviousScrollerY = currentScrollerY;
1145         if (scroller.isFinished()) {
1146             onScrollerFinished(scroller);
1147         } else {
1148             invalidate();
1149         }
1150     }
1151 
1152     @Override
setEnabled(boolean enabled)1153     public void setEnabled(boolean enabled) {
1154         super.setEnabled(enabled);
1155         if (!mHasSelectorWheel) {
1156             mIncrementButton.setEnabled(enabled);
1157         }
1158         if (!mHasSelectorWheel) {
1159             mDecrementButton.setEnabled(enabled);
1160         }
1161         mInputText.setEnabled(enabled);
1162     }
1163 
1164     @Override
scrollBy(int x, int y)1165     public void scrollBy(int x, int y) {
1166         int[] selectorIndices = mSelectorIndices;
1167         int startScrollOffset = mCurrentScrollOffset;
1168         if (!mWrapSelectorWheel && y > 0
1169                 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
1170             mCurrentScrollOffset = mInitialScrollOffset;
1171             return;
1172         }
1173         if (!mWrapSelectorWheel && y < 0
1174                 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
1175             mCurrentScrollOffset = mInitialScrollOffset;
1176             return;
1177         }
1178         mCurrentScrollOffset += y;
1179         while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
1180             mCurrentScrollOffset -= mSelectorElementHeight;
1181             decrementSelectorIndices(selectorIndices);
1182             setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
1183             if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
1184                 mCurrentScrollOffset = mInitialScrollOffset;
1185             }
1186         }
1187         while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
1188             mCurrentScrollOffset += mSelectorElementHeight;
1189             incrementSelectorIndices(selectorIndices);
1190             setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
1191             if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
1192                 mCurrentScrollOffset = mInitialScrollOffset;
1193             }
1194         }
1195         if (startScrollOffset != mCurrentScrollOffset) {
1196             onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset);
1197         }
1198     }
1199 
1200     @Override
computeVerticalScrollOffset()1201     protected int computeVerticalScrollOffset() {
1202         return mCurrentScrollOffset;
1203     }
1204 
1205     @Override
computeVerticalScrollRange()1206     protected int computeVerticalScrollRange() {
1207         return (mMaxValue - mMinValue + 1) * mSelectorElementHeight;
1208     }
1209 
1210     @Override
computeVerticalScrollExtent()1211     protected int computeVerticalScrollExtent() {
1212         return getHeight();
1213     }
1214 
1215     @Override
getSolidColor()1216     public int getSolidColor() {
1217         return mSolidColor;
1218     }
1219 
1220     /**
1221      * Sets the listener to be notified on change of the current value.
1222      *
1223      * @param onValueChangedListener The listener.
1224      */
setOnValueChangedListener(OnValueChangeListener onValueChangedListener)1225     public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
1226         mOnValueChangeListener = onValueChangedListener;
1227     }
1228 
1229     /**
1230      * Set listener to be notified for scroll state changes.
1231      *
1232      * @param onScrollListener The listener.
1233      */
setOnScrollListener(OnScrollListener onScrollListener)1234     public void setOnScrollListener(OnScrollListener onScrollListener) {
1235         mOnScrollListener = onScrollListener;
1236     }
1237 
1238     /**
1239      * Set the formatter to be used for formatting the current value.
1240      * <p>
1241      * Note: If you have provided alternative values for the values this
1242      * formatter is never invoked.
1243      * </p>
1244      *
1245      * @param formatter The formatter object. If formatter is <code>null</code>,
1246      *            {@link String#valueOf(int)} will be used.
1247      *@see #setDisplayedValues(String[])
1248      */
setFormatter(Formatter formatter)1249     public void setFormatter(Formatter formatter) {
1250         if (formatter == mFormatter) {
1251             return;
1252         }
1253         mFormatter = formatter;
1254         initializeSelectorWheelIndices();
1255         updateInputTextView();
1256     }
1257 
1258     /**
1259      * Set the current value for the number picker.
1260      * <p>
1261      * If the argument is less than the {@link NumberPicker#getMinValue()} and
1262      * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
1263      * current value is set to the {@link NumberPicker#getMinValue()} value.
1264      * </p>
1265      * <p>
1266      * If the argument is less than the {@link NumberPicker#getMinValue()} and
1267      * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
1268      * current value is set to the {@link NumberPicker#getMaxValue()} value.
1269      * </p>
1270      * <p>
1271      * If the argument is more than the {@link NumberPicker#getMaxValue()} and
1272      * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
1273      * current value is set to the {@link NumberPicker#getMaxValue()} value.
1274      * </p>
1275      * <p>
1276      * If the argument is more than the {@link NumberPicker#getMaxValue()} and
1277      * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
1278      * current value is set to the {@link NumberPicker#getMinValue()} value.
1279      * </p>
1280      *
1281      * @param value The current value.
1282      * @see #setWrapSelectorWheel(boolean)
1283      * @see #setMinValue(int)
1284      * @see #setMaxValue(int)
1285      */
setValue(int value)1286     public void setValue(int value) {
1287         setValueInternal(value, false);
1288     }
1289 
1290     @Override
performClick()1291     public boolean performClick() {
1292         if (!mHasSelectorWheel) {
1293             return super.performClick();
1294         } else if (!super.performClick()) {
1295             showSoftInput();
1296         }
1297         return true;
1298     }
1299 
1300     @Override
performLongClick()1301     public boolean performLongClick() {
1302         if (!mHasSelectorWheel) {
1303             return super.performLongClick();
1304         } else if (!super.performLongClick()) {
1305             showSoftInput();
1306             mIgnoreMoveEvents = true;
1307         }
1308         return true;
1309     }
1310 
1311     /**
1312      * Shows the soft input for its input text.
1313      */
showSoftInput()1314     private void showSoftInput() {
1315         InputMethodManager inputMethodManager =
1316                 getContext().getSystemService(InputMethodManager.class);
1317         if (inputMethodManager != null) {
1318             if (mHasSelectorWheel) {
1319                 mInputText.setVisibility(View.VISIBLE);
1320             }
1321             mInputText.requestFocus();
1322             inputMethodManager.showSoftInput(mInputText, 0);
1323         }
1324     }
1325 
1326     /**
1327      * Hides the soft input if it is active for the input text.
1328      */
hideSoftInput()1329     private void hideSoftInput() {
1330         InputMethodManager inputMethodManager =
1331                 getContext().getSystemService(InputMethodManager.class);
1332         if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
1333             inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
1334         }
1335         if (mHasSelectorWheel) {
1336             mInputText.setVisibility(View.INVISIBLE);
1337         }
1338     }
1339 
1340     /**
1341      * Computes the max width if no such specified as an attribute.
1342      */
tryComputeMaxWidth()1343     private void tryComputeMaxWidth() {
1344         if (!mComputeMaxWidth) {
1345             return;
1346         }
1347         int maxTextWidth = 0;
1348         if (mDisplayedValues == null) {
1349             float maxDigitWidth = 0;
1350             for (int i = 0; i <= 9; i++) {
1351                 final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
1352                 if (digitWidth > maxDigitWidth) {
1353                     maxDigitWidth = digitWidth;
1354                 }
1355             }
1356             int numberOfDigits = 0;
1357             int current = mMaxValue;
1358             while (current > 0) {
1359                 numberOfDigits++;
1360                 current = current / 10;
1361             }
1362             maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
1363         } else {
1364             final int valueCount = mDisplayedValues.length;
1365             for (int i = 0; i < valueCount; i++) {
1366                 final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
1367                 if (textWidth > maxTextWidth) {
1368                     maxTextWidth = (int) textWidth;
1369                 }
1370             }
1371         }
1372         maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
1373         if (mMaxWidth != maxTextWidth) {
1374             if (maxTextWidth > mMinWidth) {
1375                 mMaxWidth = maxTextWidth;
1376             } else {
1377                 mMaxWidth = mMinWidth;
1378             }
1379             invalidate();
1380         }
1381     }
1382 
1383     /**
1384      * Gets whether the selector wheel wraps when reaching the min/max value.
1385      *
1386      * @return True if the selector wheel wraps.
1387      *
1388      * @see #getMinValue()
1389      * @see #getMaxValue()
1390      */
getWrapSelectorWheel()1391     public boolean getWrapSelectorWheel() {
1392         return mWrapSelectorWheel;
1393     }
1394 
1395     /**
1396      * Sets whether the selector wheel shown during flinging/scrolling should
1397      * wrap around the {@link NumberPicker#getMinValue()} and
1398      * {@link NumberPicker#getMaxValue()} values.
1399      * <p>
1400      * By default if the range (max - min) is more than the number of items shown
1401      * on the selector wheel the selector wheel wrapping is enabled.
1402      * </p>
1403      * <p>
1404      * <strong>Note:</strong> If the number of items, i.e. the range (
1405      * {@link #getMaxValue()} - {@link #getMinValue()}) is less than
1406      * the number of items shown on the selector wheel, the selector wheel will
1407      * not wrap. Hence, in such a case calling this method is a NOP.
1408      * </p>
1409      *
1410      * @param wrapSelectorWheel Whether to wrap.
1411      */
setWrapSelectorWheel(boolean wrapSelectorWheel)1412     public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
1413         mWrapSelectorWheelPreferred = wrapSelectorWheel;
1414         updateWrapSelectorWheel();
1415 
1416     }
1417 
1418     /**
1419      * Whether or not the selector wheel should be wrapped is determined by user choice and whether
1420      * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the
1421      * latter is calculated based on min & max value set vs selector's visual length. Therefore,
1422      * this method should be called any time any of the 3 values (i.e. user choice, min and max
1423      * value) gets updated.
1424      */
updateWrapSelectorWheel()1425     private void updateWrapSelectorWheel() {
1426         final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
1427         mWrapSelectorWheel = wrappingAllowed && mWrapSelectorWheelPreferred;
1428     }
1429 
1430     /**
1431      * Sets the speed at which the numbers be incremented and decremented when
1432      * the up and down buttons are long pressed respectively.
1433      * <p>
1434      * The default value is 300 ms.
1435      * </p>
1436      *
1437      * @param intervalMillis The speed (in milliseconds) at which the numbers
1438      *            will be incremented and decremented.
1439      */
setOnLongPressUpdateInterval(long intervalMillis)1440     public void setOnLongPressUpdateInterval(long intervalMillis) {
1441         mLongPressUpdateInterval = intervalMillis;
1442     }
1443 
1444     /**
1445      * Returns the value of the picker.
1446      *
1447      * @return The value.
1448      */
getValue()1449     public int getValue() {
1450         return mValue;
1451     }
1452 
1453     /**
1454      * Returns the min value of the picker.
1455      *
1456      * @return The min value
1457      */
getMinValue()1458     public int getMinValue() {
1459         return mMinValue;
1460     }
1461 
1462     /**
1463      * Sets the min value of the picker.
1464      *
1465      * @param minValue The min value inclusive.
1466      *
1467      * <strong>Note:</strong> The length of the displayed values array
1468      * set via {@link #setDisplayedValues(String[])} must be equal to the
1469      * range of selectable numbers which is equal to
1470      * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
1471      */
setMinValue(int minValue)1472     public void setMinValue(int minValue) {
1473         if (mMinValue == minValue) {
1474             return;
1475         }
1476         if (minValue < 0) {
1477             throw new IllegalArgumentException("minValue must be >= 0");
1478         }
1479         mMinValue = minValue;
1480         if (mMinValue > mValue) {
1481             mValue = mMinValue;
1482         }
1483         updateWrapSelectorWheel();
1484         initializeSelectorWheelIndices();
1485         updateInputTextView();
1486         tryComputeMaxWidth();
1487         invalidate();
1488     }
1489 
1490     /**
1491      * Returns the max value of the picker.
1492      *
1493      * @return The max value.
1494      */
getMaxValue()1495     public int getMaxValue() {
1496         return mMaxValue;
1497     }
1498 
1499     /**
1500      * Sets the max value of the picker.
1501      *
1502      * @param maxValue The max value inclusive.
1503      *
1504      * <strong>Note:</strong> The length of the displayed values array
1505      * set via {@link #setDisplayedValues(String[])} must be equal to the
1506      * range of selectable numbers which is equal to
1507      * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
1508      */
setMaxValue(int maxValue)1509     public void setMaxValue(int maxValue) {
1510         if (mMaxValue == maxValue) {
1511             return;
1512         }
1513         if (maxValue < 0) {
1514             throw new IllegalArgumentException("maxValue must be >= 0");
1515         }
1516         mMaxValue = maxValue;
1517         if (mMaxValue < mValue) {
1518             mValue = mMaxValue;
1519         }
1520         updateWrapSelectorWheel();
1521         initializeSelectorWheelIndices();
1522         updateInputTextView();
1523         tryComputeMaxWidth();
1524         invalidate();
1525     }
1526 
1527     /**
1528      * Gets the values to be displayed instead of string values.
1529      *
1530      * @return The displayed values.
1531      */
getDisplayedValues()1532     public String[] getDisplayedValues() {
1533         return mDisplayedValues;
1534     }
1535 
1536     /**
1537      * Sets the values to be displayed.
1538      *
1539      * @param displayedValues The displayed values.
1540      *
1541      * <strong>Note:</strong> The length of the displayed values array
1542      * must be equal to the range of selectable numbers which is equal to
1543      * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
1544      */
setDisplayedValues(String[] displayedValues)1545     public void setDisplayedValues(String[] displayedValues) {
1546         if (mDisplayedValues == displayedValues) {
1547             return;
1548         }
1549         mDisplayedValues = displayedValues;
1550         if (mDisplayedValues != null) {
1551             // Allow text entry rather than strictly numeric entry.
1552             mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
1553                     | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
1554         } else {
1555             mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1556         }
1557         updateInputTextView();
1558         initializeSelectorWheelIndices();
1559         tryComputeMaxWidth();
1560     }
1561 
1562     /**
1563      * Retrieves the displayed value for the current selection in this picker.
1564      *
1565      * @hide
1566      */
1567     @TestApi
getDisplayedValueForCurrentSelection()1568     public CharSequence getDisplayedValueForCurrentSelection() {
1569         // The cache field itself is initialized at declaration time, and since it's final, it
1570         // can't be null here. The cache is updated in ensureCachedScrollSelectorValue which is
1571         // called, directly or indirectly, on every call to setDisplayedValues, setFormatter,
1572         // setMinValue, setMaxValue and setValue, as well as user-driven interaction with the
1573         // picker. As such, the contents of the cache are always synced to the latest state of
1574         // the widget.
1575         return mSelectorIndexToStringCache.get(getValue());
1576     }
1577 
1578     /**
1579      * Set the height for the divider that separates the currently selected value from the others.
1580      * @param height The height to be set
1581      */
setSelectionDividerHeight(@ntRangefrom = 0) @x int height)1582     public void setSelectionDividerHeight(@IntRange(from = 0) @Px int height) {
1583         mSelectionDividerHeight = height;
1584         invalidate();
1585     }
1586 
1587     /**
1588      * Retrieve the height for the divider that separates the currently selected value from the
1589      * others.
1590      * @return The height of the divider
1591      */
1592     @Px
getSelectionDividerHeight()1593     public int getSelectionDividerHeight() {
1594         return mSelectionDividerHeight;
1595     }
1596 
1597     @Override
getTopFadingEdgeStrength()1598     protected float getTopFadingEdgeStrength() {
1599         return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1600     }
1601 
1602     @Override
getBottomFadingEdgeStrength()1603     protected float getBottomFadingEdgeStrength() {
1604         return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1605     }
1606 
1607     @Override
onDetachedFromWindow()1608     protected void onDetachedFromWindow() {
1609         super.onDetachedFromWindow();
1610         removeAllCallbacks();
1611     }
1612 
1613     @CallSuper
1614     @Override
drawableStateChanged()1615     protected void drawableStateChanged() {
1616         super.drawableStateChanged();
1617 
1618         final Drawable selectionDivider = mSelectionDivider;
1619         if (selectionDivider != null && selectionDivider.isStateful()
1620                 && selectionDivider.setState(getDrawableState())) {
1621             invalidateDrawable(selectionDivider);
1622         }
1623     }
1624 
1625     @CallSuper
1626     @Override
jumpDrawablesToCurrentState()1627     public void jumpDrawablesToCurrentState() {
1628         super.jumpDrawablesToCurrentState();
1629 
1630         if (mSelectionDivider != null) {
1631             mSelectionDivider.jumpToCurrentState();
1632         }
1633     }
1634 
1635     /** @hide */
1636     @Override
onResolveDrawables(@esolvedLayoutDir int layoutDirection)1637     public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
1638         super.onResolveDrawables(layoutDirection);
1639 
1640         if (mSelectionDivider != null) {
1641             mSelectionDivider.setLayoutDirection(layoutDirection);
1642         }
1643     }
1644 
1645     @Override
onDraw(Canvas canvas)1646     protected void onDraw(Canvas canvas) {
1647         if (!mHasSelectorWheel) {
1648             super.onDraw(canvas);
1649             return;
1650         }
1651         final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true;
1652         float x = (mRight - mLeft) / 2;
1653         float y = mCurrentScrollOffset;
1654 
1655         // draw the virtual buttons pressed state if needed
1656         if (showSelectorWheel && mVirtualButtonPressedDrawable != null
1657                 && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
1658             if (mDecrementVirtualButtonPressed) {
1659                 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
1660                 mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop);
1661                 mVirtualButtonPressedDrawable.draw(canvas);
1662             }
1663             if (mIncrementVirtualButtonPressed) {
1664                 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
1665                 mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight,
1666                         mBottom);
1667                 mVirtualButtonPressedDrawable.draw(canvas);
1668             }
1669         }
1670 
1671         // draw the selector wheel
1672         int[] selectorIndices = mSelectorIndices;
1673         for (int i = 0; i < selectorIndices.length; i++) {
1674             int selectorIndex = selectorIndices[i];
1675             String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1676             // Do not draw the middle item if input is visible since the input
1677             // is shown only if the wheel is static and it covers the middle
1678             // item. Otherwise, if the user starts editing the text via the
1679             // IME he may see a dimmed version of the old value intermixed
1680             // with the new one.
1681             if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) ||
1682                 (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) {
1683                 canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
1684             }
1685             y += mSelectorElementHeight;
1686         }
1687 
1688         // draw the selection dividers
1689         if (showSelectorWheel && mSelectionDivider != null) {
1690             // draw the top divider
1691             int topOfTopDivider = mTopSelectionDividerTop;
1692             int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
1693             mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
1694             mSelectionDivider.draw(canvas);
1695 
1696             // draw the bottom divider
1697             int bottomOfBottomDivider = mBottomSelectionDividerBottom;
1698             int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
1699             mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
1700             mSelectionDivider.draw(canvas);
1701         }
1702     }
1703 
1704     /** @hide */
1705     @Override
onInitializeAccessibilityEventInternal(AccessibilityEvent event)1706     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
1707         super.onInitializeAccessibilityEventInternal(event);
1708         event.setClassName(NumberPicker.class.getName());
1709         event.setScrollable(true);
1710         event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
1711         event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
1712     }
1713 
1714     @Override
getAccessibilityNodeProvider()1715     public AccessibilityNodeProvider getAccessibilityNodeProvider() {
1716         if (!mHasSelectorWheel) {
1717             return super.getAccessibilityNodeProvider();
1718         }
1719         if (mAccessibilityNodeProvider == null) {
1720             mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
1721         }
1722         return mAccessibilityNodeProvider;
1723     }
1724 
1725     /**
1726      * Sets the text color for all the states (normal, selected, focused) to be the given color.
1727      *
1728      * @param color A color value in the form 0xAARRGGBB.
1729      */
setTextColor(@olorInt int color)1730     public void setTextColor(@ColorInt int color) {
1731         mSelectorWheelPaint.setColor(color);
1732         mInputText.setTextColor(color);
1733         invalidate();
1734     }
1735 
1736     /**
1737      * @return the text color.
1738      */
1739     @ColorInt
getTextColor()1740     public int getTextColor() {
1741         return mSelectorWheelPaint.getColor();
1742     }
1743 
1744     /**
1745      * Sets the text size to the given value. This value must be > 0
1746      *
1747      * @param size The size in pixel units.
1748      */
setTextSize(@loatRangefrom = 0.0, fromInclusive = false) float size)1749     public void setTextSize(@FloatRange(from = 0.0, fromInclusive = false) float size) {
1750         mSelectorWheelPaint.setTextSize(size);
1751         mInputText.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
1752         invalidate();
1753     }
1754 
1755     /**
1756      * @return the size (in pixels) of the text size in this NumberPicker.
1757      */
1758     @FloatRange(from = 0.0, fromInclusive = false)
getTextSize()1759     public float getTextSize() {
1760         return mSelectorWheelPaint.getTextSize();
1761     }
1762 
1763     /**
1764      * Makes a measure spec that tries greedily to use the max value.
1765      *
1766      * @param measureSpec The measure spec.
1767      * @param maxSize The max value for the size.
1768      * @return A measure spec greedily imposing the max size.
1769      */
makeMeasureSpec(int measureSpec, int maxSize)1770     private int makeMeasureSpec(int measureSpec, int maxSize) {
1771         if (maxSize == SIZE_UNSPECIFIED) {
1772             return measureSpec;
1773         }
1774         final int size = MeasureSpec.getSize(measureSpec);
1775         final int mode = MeasureSpec.getMode(measureSpec);
1776         switch (mode) {
1777             case MeasureSpec.EXACTLY:
1778                 return measureSpec;
1779             case MeasureSpec.AT_MOST:
1780                 return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
1781             case MeasureSpec.UNSPECIFIED:
1782                 return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
1783             default:
1784                 throw new IllegalArgumentException("Unknown measure mode: " + mode);
1785         }
1786     }
1787 
1788     /**
1789      * Utility to reconcile a desired size and state, with constraints imposed
1790      * by a MeasureSpec. Tries to respect the min size, unless a different size
1791      * is imposed by the constraints.
1792      *
1793      * @param minSize The minimal desired size.
1794      * @param measuredSize The currently measured size.
1795      * @param measureSpec The current measure spec.
1796      * @return The resolved size and state.
1797      */
resolveSizeAndStateRespectingMinSize( int minSize, int measuredSize, int measureSpec)1798     private int resolveSizeAndStateRespectingMinSize(
1799             int minSize, int measuredSize, int measureSpec) {
1800         if (minSize != SIZE_UNSPECIFIED) {
1801             final int desiredWidth = Math.max(minSize, measuredSize);
1802             return resolveSizeAndState(desiredWidth, measureSpec, 0);
1803         } else {
1804             return measuredSize;
1805         }
1806     }
1807 
1808     /**
1809      * Resets the selector indices and clear the cached string representation of
1810      * these indices.
1811      */
1812     @UnsupportedAppUsage
initializeSelectorWheelIndices()1813     private void initializeSelectorWheelIndices() {
1814         mSelectorIndexToStringCache.clear();
1815         int[] selectorIndices = mSelectorIndices;
1816         int current = getValue();
1817         for (int i = 0; i < mSelectorIndices.length; i++) {
1818             int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1819             if (mWrapSelectorWheel) {
1820                 selectorIndex = getWrappedSelectorIndex(selectorIndex);
1821             }
1822             selectorIndices[i] = selectorIndex;
1823             ensureCachedScrollSelectorValue(selectorIndices[i]);
1824         }
1825     }
1826 
1827     /**
1828      * Sets the current value of this NumberPicker.
1829      *
1830      * @param current The new value of the NumberPicker.
1831      * @param notifyChange Whether to notify if the current value changed.
1832      */
setValueInternal(int current, boolean notifyChange)1833     private void setValueInternal(int current, boolean notifyChange) {
1834         if (mValue == current) {
1835             return;
1836         }
1837         // Wrap around the values if we go past the start or end
1838         if (mWrapSelectorWheel) {
1839             current = getWrappedSelectorIndex(current);
1840         } else {
1841             current = Math.max(current, mMinValue);
1842             current = Math.min(current, mMaxValue);
1843         }
1844         int previous = mValue;
1845         mValue = current;
1846         // If we're flinging, we'll update the text view at the end when it becomes visible
1847         if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
1848             updateInputTextView();
1849         }
1850         if (notifyChange) {
1851             notifyChange(previous, current);
1852         }
1853         initializeSelectorWheelIndices();
1854         invalidate();
1855     }
1856 
1857     /**
1858      * Changes the current value by one which is increment or
1859      * decrement based on the passes argument.
1860      * decrement the current value.
1861      *
1862      * @param increment True to increment, false to decrement.
1863      */
1864      @UnsupportedAppUsage
changeValueByOne(boolean increment)1865      private void changeValueByOne(boolean increment) {
1866         if (mHasSelectorWheel) {
1867             hideSoftInput();
1868             if (!moveToFinalScrollerPosition(mFlingScroller)) {
1869                 moveToFinalScrollerPosition(mAdjustScroller);
1870             }
1871             mPreviousScrollerY = 0;
1872             if (increment) {
1873                 mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION);
1874             } else {
1875                 mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION);
1876             }
1877             invalidate();
1878         } else {
1879             if (increment) {
1880                 setValueInternal(mValue + 1, true);
1881             } else {
1882                 setValueInternal(mValue - 1, true);
1883             }
1884         }
1885     }
1886 
initializeSelectorWheel()1887     private void initializeSelectorWheel() {
1888         initializeSelectorWheelIndices();
1889         int[] selectorIndices = mSelectorIndices;
1890         int totalTextHeight = selectorIndices.length * mTextSize;
1891         float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
1892         float textGapCount = selectorIndices.length;
1893         mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
1894         mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
1895         // Ensure that the middle item is positioned the same as the text in
1896         // mInputText
1897         int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
1898         mInitialScrollOffset = editTextTextPosition
1899                 - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
1900         mCurrentScrollOffset = mInitialScrollOffset;
1901         updateInputTextView();
1902     }
1903 
initializeFadingEdges()1904     private void initializeFadingEdges() {
1905         setVerticalFadingEdgeEnabled(true);
1906         setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
1907     }
1908 
1909     /**
1910      * Callback invoked upon completion of a given <code>scroller</code>.
1911      */
onScrollerFinished(Scroller scroller)1912     private void onScrollerFinished(Scroller scroller) {
1913         if (scroller == mFlingScroller) {
1914             ensureScrollWheelAdjusted();
1915             updateInputTextView();
1916             onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1917         } else {
1918             if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
1919                 updateInputTextView();
1920             }
1921         }
1922     }
1923 
1924     /**
1925      * Handles transition to a given <code>scrollState</code>
1926      */
onScrollStateChange(int scrollState)1927     private void onScrollStateChange(int scrollState) {
1928         if (mScrollState == scrollState) {
1929             return;
1930         }
1931         mScrollState = scrollState;
1932         if (mOnScrollListener != null) {
1933             mOnScrollListener.onScrollStateChange(this, scrollState);
1934         }
1935     }
1936 
1937     /**
1938      * Flings the selector with the given <code>velocityY</code>.
1939      */
fling(int velocityY)1940     private void fling(int velocityY) {
1941         mPreviousScrollerY = 0;
1942 
1943         if (velocityY > 0) {
1944             mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1945         } else {
1946             mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1947         }
1948 
1949         invalidate();
1950     }
1951 
1952     /**
1953      * @return The wrapped index <code>selectorIndex</code> value.
1954      */
getWrappedSelectorIndex(int selectorIndex)1955     private int getWrappedSelectorIndex(int selectorIndex) {
1956         if (selectorIndex > mMaxValue) {
1957             return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
1958         } else if (selectorIndex < mMinValue) {
1959             return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
1960         }
1961         return selectorIndex;
1962     }
1963 
1964     /**
1965      * Increments the <code>selectorIndices</code> whose string representations
1966      * will be displayed in the selector.
1967      */
incrementSelectorIndices(int[] selectorIndices)1968     private void incrementSelectorIndices(int[] selectorIndices) {
1969         for (int i = 0; i < selectorIndices.length - 1; i++) {
1970             selectorIndices[i] = selectorIndices[i + 1];
1971         }
1972         int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1973         if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
1974             nextScrollSelectorIndex = mMinValue;
1975         }
1976         selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1977         ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1978     }
1979 
1980     /**
1981      * Decrements the <code>selectorIndices</code> whose string representations
1982      * will be displayed in the selector.
1983      */
decrementSelectorIndices(int[] selectorIndices)1984     private void decrementSelectorIndices(int[] selectorIndices) {
1985         for (int i = selectorIndices.length - 1; i > 0; i--) {
1986             selectorIndices[i] = selectorIndices[i - 1];
1987         }
1988         int nextScrollSelectorIndex = selectorIndices[1] - 1;
1989         if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
1990             nextScrollSelectorIndex = mMaxValue;
1991         }
1992         selectorIndices[0] = nextScrollSelectorIndex;
1993         ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1994     }
1995 
1996     /**
1997      * Ensures we have a cached string representation of the given <code>
1998      * selectorIndex</code> to avoid multiple instantiations of the same string.
1999      */
ensureCachedScrollSelectorValue(int selectorIndex)2000     private void ensureCachedScrollSelectorValue(int selectorIndex) {
2001         SparseArray<String> cache = mSelectorIndexToStringCache;
2002         String scrollSelectorValue = cache.get(selectorIndex);
2003         if (scrollSelectorValue != null) {
2004             return;
2005         }
2006         if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
2007             scrollSelectorValue = "";
2008         } else {
2009             if (mDisplayedValues != null) {
2010                 int displayedValueIndex = selectorIndex - mMinValue;
2011                 scrollSelectorValue = mDisplayedValues[displayedValueIndex];
2012             } else {
2013                 scrollSelectorValue = formatNumber(selectorIndex);
2014             }
2015         }
2016         cache.put(selectorIndex, scrollSelectorValue);
2017     }
2018 
formatNumber(int value)2019     private String formatNumber(int value) {
2020         return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
2021     }
2022 
validateInputTextView(View v)2023     private void validateInputTextView(View v) {
2024         String str = String.valueOf(((TextView) v).getText());
2025         if (TextUtils.isEmpty(str)) {
2026             // Restore to the old value as we don't allow empty values
2027             updateInputTextView();
2028         } else {
2029             // Check the new value and ensure it's in range
2030             int current = getSelectedPos(str.toString());
2031             setValueInternal(current, true);
2032         }
2033     }
2034 
2035     /**
2036      * Updates the view of this NumberPicker. If displayValues were specified in
2037      * the string corresponding to the index specified by the current value will
2038      * be returned. Otherwise, the formatter specified in {@link #setFormatter}
2039      * will be used to format the number.
2040      *
2041      * @return Whether the text was updated.
2042      */
updateInputTextView()2043     private boolean updateInputTextView() {
2044         /*
2045          * If we don't have displayed values then use the current number else
2046          * find the correct value in the displayed values for the current
2047          * number.
2048          */
2049         String text = (mDisplayedValues == null) ? formatNumber(mValue)
2050                 : mDisplayedValues[mValue - mMinValue];
2051         if (!TextUtils.isEmpty(text)) {
2052             CharSequence beforeText = mInputText.getText();
2053             if (!text.equals(beforeText.toString())) {
2054                 mInputText.setText(text);
2055                 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
2056                     AccessibilityEvent event = AccessibilityEvent.obtain(
2057                             AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
2058                     mInputText.onInitializeAccessibilityEvent(event);
2059                     mInputText.onPopulateAccessibilityEvent(event);
2060                     event.setFromIndex(0);
2061                     event.setRemovedCount(beforeText.length());
2062                     event.setAddedCount(text.length());
2063                     event.setBeforeText(beforeText);
2064                     event.setSource(NumberPicker.this,
2065                             AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT);
2066                     requestSendAccessibilityEvent(NumberPicker.this, event);
2067                 }
2068                 return true;
2069             }
2070         }
2071 
2072         return false;
2073     }
2074 
2075     /**
2076      * Notifies the listener, if registered, of a change of the value of this
2077      * NumberPicker.
2078      */
notifyChange(int previous, int current)2079     private void notifyChange(int previous, int current) {
2080         if (mOnValueChangeListener != null) {
2081             mOnValueChangeListener.onValueChange(this, previous, mValue);
2082         }
2083     }
2084 
2085     /**
2086      * Posts a command for changing the current value by one.
2087      *
2088      * @param increment Whether to increment or decrement the value.
2089      */
postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis)2090     private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
2091         if (mChangeCurrentByOneFromLongPressCommand == null) {
2092             mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
2093         } else {
2094             removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
2095         }
2096         mChangeCurrentByOneFromLongPressCommand.setStep(increment);
2097         postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
2098     }
2099 
2100     /**
2101      * Removes the command for changing the current value by one.
2102      */
removeChangeCurrentByOneFromLongPress()2103     private void removeChangeCurrentByOneFromLongPress() {
2104         if (mChangeCurrentByOneFromLongPressCommand != null) {
2105             removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
2106         }
2107     }
2108 
2109     /**
2110      * Posts a command for beginning an edit of the current value via IME on
2111      * long press.
2112      */
postBeginSoftInputOnLongPressCommand()2113     private void postBeginSoftInputOnLongPressCommand() {
2114         if (mBeginSoftInputOnLongPressCommand == null) {
2115             mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
2116         } else {
2117             removeCallbacks(mBeginSoftInputOnLongPressCommand);
2118         }
2119         postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout());
2120     }
2121 
2122     /**
2123      * Removes the command for beginning an edit of the current value via IME.
2124      */
removeBeginSoftInputCommand()2125     private void removeBeginSoftInputCommand() {
2126         if (mBeginSoftInputOnLongPressCommand != null) {
2127             removeCallbacks(mBeginSoftInputOnLongPressCommand);
2128         }
2129     }
2130 
2131     /**
2132      * Removes all pending callback from the message queue.
2133      */
removeAllCallbacks()2134     private void removeAllCallbacks() {
2135         if (mChangeCurrentByOneFromLongPressCommand != null) {
2136             removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
2137         }
2138         if (mSetSelectionCommand != null) {
2139             mSetSelectionCommand.cancel();
2140         }
2141         if (mBeginSoftInputOnLongPressCommand != null) {
2142             removeCallbacks(mBeginSoftInputOnLongPressCommand);
2143         }
2144         mPressedStateHelper.cancel();
2145     }
2146 
2147     /**
2148      * @return The selected index given its displayed <code>value</code>.
2149      */
getSelectedPos(String value)2150     private int getSelectedPos(String value) {
2151         if (mDisplayedValues == null) {
2152             try {
2153                 return Integer.parseInt(value);
2154             } catch (NumberFormatException e) {
2155                 // Ignore as if it's not a number we don't care
2156             }
2157         } else {
2158             for (int i = 0; i < mDisplayedValues.length; i++) {
2159                 // Don't force the user to type in jan when ja will do
2160                 value = value.toLowerCase();
2161                 if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
2162                     return mMinValue + i;
2163                 }
2164             }
2165 
2166             /*
2167              * The user might have typed in a number into the month field i.e.
2168              * 10 instead of OCT so support that too.
2169              */
2170             try {
2171                 return Integer.parseInt(value);
2172             } catch (NumberFormatException e) {
2173 
2174                 // Ignore as if it's not a number we don't care
2175             }
2176         }
2177         return mMinValue;
2178     }
2179 
2180     /**
2181      * Posts a {@link SetSelectionCommand} from the given
2182      * {@code selectionStart} to {@code selectionEnd}.
2183      */
postSetSelectionCommand(int selectionStart, int selectionEnd)2184     private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
2185         if (mSetSelectionCommand == null) {
2186             mSetSelectionCommand = new SetSelectionCommand(mInputText);
2187         }
2188         mSetSelectionCommand.post(selectionStart, selectionEnd);
2189     }
2190 
2191     /**
2192      * The numbers accepted by the input text's {@link Filter}
2193      */
2194     private static final char[] DIGIT_CHARACTERS = new char[] {
2195             // Latin digits are the common case
2196             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
2197             // Arabic-Indic
2198             '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
2199             , '\u0669',
2200             // Extended Arabic-Indic
2201             '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
2202             , '\u06f9',
2203             // Hindi and Marathi (Devanagari script)
2204             '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e'
2205             , '\u096f',
2206             // Bengali
2207             '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee'
2208             , '\u09ef',
2209             // Kannada
2210             '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee'
2211             , '\u0cef'
2212     };
2213 
2214     /**
2215      * Filter for accepting only valid indices or prefixes of the string
2216      * representation of valid indices.
2217      */
2218     class InputTextFilter extends NumberKeyListener {
2219 
2220         // XXX This doesn't allow for range limits when controlled by a
2221         // soft input method!
getInputType()2222         public int getInputType() {
2223             return InputType.TYPE_CLASS_TEXT;
2224         }
2225 
2226         @Override
getAcceptedChars()2227         protected char[] getAcceptedChars() {
2228             return DIGIT_CHARACTERS;
2229         }
2230 
2231         @Override
filter( CharSequence source, int start, int end, Spanned dest, int dstart, int dend)2232         public CharSequence filter(
2233                 CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
2234             // We don't know what the output will be, so always cancel any
2235             // pending set selection command.
2236             if (mSetSelectionCommand != null) {
2237                 mSetSelectionCommand.cancel();
2238             }
2239 
2240             if (mDisplayedValues == null) {
2241                 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
2242                 if (filtered == null) {
2243                     filtered = source.subSequence(start, end);
2244                 }
2245 
2246                 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
2247                         + dest.subSequence(dend, dest.length());
2248 
2249                 if ("".equals(result)) {
2250                     return result;
2251                 }
2252                 int val = getSelectedPos(result);
2253 
2254                 /*
2255                  * Ensure the user can't type in a value greater than the max
2256                  * allowed. We have to allow less than min as the user might
2257                  * want to delete some numbers and then type a new number.
2258                  * And prevent multiple-"0" that exceeds the length of upper
2259                  * bound number.
2260                  */
2261                 if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) {
2262                     return "";
2263                 } else {
2264                     return filtered;
2265                 }
2266             } else {
2267                 CharSequence filtered = String.valueOf(source.subSequence(start, end));
2268                 if (TextUtils.isEmpty(filtered)) {
2269                     return "";
2270                 }
2271                 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
2272                         + dest.subSequence(dend, dest.length());
2273                 String str = String.valueOf(result).toLowerCase();
2274                 for (String val : mDisplayedValues) {
2275                     String valLowerCase = val.toLowerCase();
2276                     if (valLowerCase.startsWith(str)) {
2277                         postSetSelectionCommand(result.length(), val.length());
2278                         return val.subSequence(dstart, val.length());
2279                     }
2280                 }
2281                 return "";
2282             }
2283         }
2284     }
2285 
2286     /**
2287      * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
2288      * middle element is in the middle of the widget.
2289      *
2290      * @return Whether an adjustment has been made.
2291      */
ensureScrollWheelAdjusted()2292     private boolean ensureScrollWheelAdjusted() {
2293         // adjust to the closest value
2294         int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
2295         if (deltaY != 0) {
2296             mPreviousScrollerY = 0;
2297             if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
2298                 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
2299             }
2300             mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
2301             invalidate();
2302             return true;
2303         }
2304         return false;
2305     }
2306 
2307     class PressedStateHelper implements Runnable {
2308         public static final int BUTTON_INCREMENT = 1;
2309         public static final int BUTTON_DECREMENT = 2;
2310 
2311         private final int MODE_PRESS = 1;
2312         private final int MODE_TAPPED = 2;
2313 
2314         private int mManagedButton;
2315         private int mMode;
2316 
cancel()2317         public void cancel() {
2318             mMode = 0;
2319             mManagedButton = 0;
2320             NumberPicker.this.removeCallbacks(this);
2321             if (mIncrementVirtualButtonPressed) {
2322                 mIncrementVirtualButtonPressed = false;
2323                 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2324             }
2325             mDecrementVirtualButtonPressed = false;
2326             if (mDecrementVirtualButtonPressed) {
2327                 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2328             }
2329         }
2330 
buttonPressDelayed(int button)2331         public void buttonPressDelayed(int button) {
2332             cancel();
2333             mMode = MODE_PRESS;
2334             mManagedButton = button;
2335             NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
2336         }
2337 
buttonTapped(int button)2338         public void buttonTapped(int button) {
2339             cancel();
2340             mMode = MODE_TAPPED;
2341             mManagedButton = button;
2342             NumberPicker.this.post(this);
2343         }
2344 
2345         @Override
run()2346         public void run() {
2347             switch (mMode) {
2348                 case MODE_PRESS: {
2349                     switch (mManagedButton) {
2350                         case BUTTON_INCREMENT: {
2351                             mIncrementVirtualButtonPressed = true;
2352                             invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2353                         } break;
2354                         case BUTTON_DECREMENT: {
2355                             mDecrementVirtualButtonPressed = true;
2356                             invalidate(0, 0, mRight, mTopSelectionDividerTop);
2357                         }
2358                     }
2359                 } break;
2360                 case MODE_TAPPED: {
2361                     switch (mManagedButton) {
2362                         case BUTTON_INCREMENT: {
2363                             if (!mIncrementVirtualButtonPressed) {
2364                                 NumberPicker.this.postDelayed(this,
2365                                         ViewConfiguration.getPressedStateDuration());
2366                             }
2367                             mIncrementVirtualButtonPressed ^= true;
2368                             invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2369                         } break;
2370                         case BUTTON_DECREMENT: {
2371                             if (!mDecrementVirtualButtonPressed) {
2372                                 NumberPicker.this.postDelayed(this,
2373                                         ViewConfiguration.getPressedStateDuration());
2374                             }
2375                             mDecrementVirtualButtonPressed ^= true;
2376                             invalidate(0, 0, mRight, mTopSelectionDividerTop);
2377                         }
2378                     }
2379                 } break;
2380             }
2381         }
2382     }
2383 
2384     /**
2385      * Command for setting the input text selection.
2386      */
2387     private static class SetSelectionCommand implements Runnable {
2388         private final EditText mInputText;
2389 
2390         private int mSelectionStart;
2391         private int mSelectionEnd;
2392 
2393         /** Whether this runnable is currently posted. */
2394         private boolean mPosted;
2395 
SetSelectionCommand(EditText inputText)2396         public SetSelectionCommand(EditText inputText) {
2397             mInputText = inputText;
2398         }
2399 
post(int selectionStart, int selectionEnd)2400         public void post(int selectionStart, int selectionEnd) {
2401             mSelectionStart = selectionStart;
2402             mSelectionEnd = selectionEnd;
2403 
2404             if (!mPosted) {
2405                 mInputText.post(this);
2406                 mPosted = true;
2407             }
2408         }
2409 
cancel()2410         public void cancel() {
2411             if (mPosted) {
2412                 mInputText.removeCallbacks(this);
2413                 mPosted = false;
2414             }
2415         }
2416 
2417         @Override
run()2418         public void run() {
2419             mPosted = false;
2420             mInputText.setSelection(mSelectionStart, mSelectionEnd);
2421         }
2422     }
2423 
2424     /**
2425      * Command for changing the current value from a long press by one.
2426      */
2427     class ChangeCurrentByOneFromLongPressCommand implements Runnable {
2428         private boolean mIncrement;
2429 
setStep(boolean increment)2430         private void setStep(boolean increment) {
2431             mIncrement = increment;
2432         }
2433 
2434         @Override
run()2435         public void run() {
2436             changeValueByOne(mIncrement);
2437             postDelayed(this, mLongPressUpdateInterval);
2438         }
2439     }
2440 
2441     /**
2442      * @hide
2443      */
2444     public static class CustomEditText extends EditText {
2445 
CustomEditText(Context context, AttributeSet attrs)2446         public CustomEditText(Context context, AttributeSet attrs) {
2447             super(context, attrs);
2448         }
2449 
2450         @Override
onEditorAction(int actionCode)2451         public void onEditorAction(int actionCode) {
2452             super.onEditorAction(actionCode);
2453             if (actionCode == EditorInfo.IME_ACTION_DONE) {
2454                 clearFocus();
2455             }
2456         }
2457     }
2458 
2459     /**
2460      * Command for beginning soft input on long press.
2461      */
2462     class BeginSoftInputOnLongPressCommand implements Runnable {
2463 
2464         @Override
run()2465         public void run() {
2466             performLongClick();
2467         }
2468     }
2469 
2470     /**
2471      * Class for managing virtual view tree rooted at this picker.
2472      */
2473     class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
2474         private static final int UNDEFINED = Integer.MIN_VALUE;
2475 
2476         private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
2477 
2478         private static final int VIRTUAL_VIEW_ID_INPUT = 2;
2479 
2480         private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
2481 
2482         private final Rect mTempRect = new Rect();
2483 
2484         private final int[] mTempArray = new int[2];
2485 
2486         private int mAccessibilityFocusedView = UNDEFINED;
2487 
2488         @Override
createAccessibilityNodeInfo(int virtualViewId)2489         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
2490             switch (virtualViewId) {
2491                 case View.NO_ID:
2492                     return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
2493                             mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
2494                 case VIRTUAL_VIEW_ID_DECREMENT:
2495                     return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
2496                             getVirtualDecrementButtonText(), mScrollX, mScrollY,
2497                             mScrollX + (mRight - mLeft),
2498                             mTopSelectionDividerTop + mSelectionDividerHeight);
2499                 case VIRTUAL_VIEW_ID_INPUT:
2500                     return createAccessibiltyNodeInfoForInputText(mScrollX,
2501                             mTopSelectionDividerTop + mSelectionDividerHeight,
2502                             mScrollX + (mRight - mLeft),
2503                             mBottomSelectionDividerBottom - mSelectionDividerHeight);
2504                 case VIRTUAL_VIEW_ID_INCREMENT:
2505                     return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
2506                             getVirtualIncrementButtonText(), mScrollX,
2507                             mBottomSelectionDividerBottom - mSelectionDividerHeight,
2508                             mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
2509             }
2510             return super.createAccessibilityNodeInfo(virtualViewId);
2511         }
2512 
2513         @Override
findAccessibilityNodeInfosByText(String searched, int virtualViewId)2514         public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
2515                 int virtualViewId) {
2516             if (TextUtils.isEmpty(searched)) {
2517                 return Collections.emptyList();
2518             }
2519             String searchedLowerCase = searched.toLowerCase();
2520             List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
2521             switch (virtualViewId) {
2522                 case View.NO_ID: {
2523                     findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2524                             VIRTUAL_VIEW_ID_DECREMENT, result);
2525                     findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2526                             VIRTUAL_VIEW_ID_INPUT, result);
2527                     findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2528                             VIRTUAL_VIEW_ID_INCREMENT, result);
2529                     return result;
2530                 }
2531                 case VIRTUAL_VIEW_ID_DECREMENT:
2532                 case VIRTUAL_VIEW_ID_INCREMENT:
2533                 case VIRTUAL_VIEW_ID_INPUT: {
2534                     findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId,
2535                             result);
2536                     return result;
2537                 }
2538             }
2539             return super.findAccessibilityNodeInfosByText(searched, virtualViewId);
2540         }
2541 
2542         @Override
performAction(int virtualViewId, int action, Bundle arguments)2543         public boolean performAction(int virtualViewId, int action, Bundle arguments) {
2544             switch (virtualViewId) {
2545                 case View.NO_ID: {
2546                     switch (action) {
2547                         case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2548                             if (mAccessibilityFocusedView != virtualViewId) {
2549                                 mAccessibilityFocusedView = virtualViewId;
2550                                 requestAccessibilityFocus();
2551                                 return true;
2552                             }
2553                         } return false;
2554                         case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2555                             if (mAccessibilityFocusedView == virtualViewId) {
2556                                 mAccessibilityFocusedView = UNDEFINED;
2557                                 clearAccessibilityFocus();
2558                                 return true;
2559                             }
2560                             return false;
2561                         }
2562                         case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
2563                         case R.id.accessibilityActionScrollDown: {
2564                             if (NumberPicker.this.isEnabled()
2565                                     && (getWrapSelectorWheel() || getValue() < getMaxValue())) {
2566                                 changeValueByOne(true);
2567                                 return true;
2568                             }
2569                         } return false;
2570                         case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
2571                         case R.id.accessibilityActionScrollUp: {
2572                             if (NumberPicker.this.isEnabled()
2573                                     && (getWrapSelectorWheel() || getValue() > getMinValue())) {
2574                                 changeValueByOne(false);
2575                                 return true;
2576                             }
2577                         } return false;
2578                     }
2579                 } break;
2580                 case VIRTUAL_VIEW_ID_INPUT: {
2581                     switch (action) {
2582                         case AccessibilityNodeInfo.ACTION_FOCUS: {
2583                             if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) {
2584                                 return mInputText.requestFocus();
2585                             }
2586                         } break;
2587                         case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
2588                             if (NumberPicker.this.isEnabled() && mInputText.isFocused()) {
2589                                 mInputText.clearFocus();
2590                                 return true;
2591                             }
2592                             return false;
2593                         }
2594                         case AccessibilityNodeInfo.ACTION_CLICK: {
2595                             if (NumberPicker.this.isEnabled()) {
2596                                 performClick();
2597                                 return true;
2598                             }
2599                             return false;
2600                         }
2601                         case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
2602                             if (NumberPicker.this.isEnabled()) {
2603                                 performLongClick();
2604                                 return true;
2605                             }
2606                             return false;
2607                         }
2608                         case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2609                             if (mAccessibilityFocusedView != virtualViewId) {
2610                                 mAccessibilityFocusedView = virtualViewId;
2611                                 sendAccessibilityEventForVirtualView(virtualViewId,
2612                                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2613                                 mInputText.invalidate();
2614                                 return true;
2615                             }
2616                         } return false;
2617                         case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2618                             if (mAccessibilityFocusedView == virtualViewId) {
2619                                 mAccessibilityFocusedView = UNDEFINED;
2620                                 sendAccessibilityEventForVirtualView(virtualViewId,
2621                                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2622                                 mInputText.invalidate();
2623                                 return true;
2624                             }
2625                         } return false;
2626                         default: {
2627                             return mInputText.performAccessibilityAction(action, arguments);
2628                         }
2629                     }
2630                 } return false;
2631                 case VIRTUAL_VIEW_ID_INCREMENT: {
2632                     switch (action) {
2633                         case AccessibilityNodeInfo.ACTION_CLICK: {
2634                             if (NumberPicker.this.isEnabled()) {
2635                                 NumberPicker.this.changeValueByOne(true);
2636                                 sendAccessibilityEventForVirtualView(virtualViewId,
2637                                         AccessibilityEvent.TYPE_VIEW_CLICKED);
2638                                 return true;
2639                             }
2640                         } return false;
2641                         case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2642                             if (mAccessibilityFocusedView != virtualViewId) {
2643                                 mAccessibilityFocusedView = virtualViewId;
2644                                 sendAccessibilityEventForVirtualView(virtualViewId,
2645                                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2646                                 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2647                                 return true;
2648                             }
2649                         } return false;
2650                         case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2651                             if (mAccessibilityFocusedView == virtualViewId) {
2652                                 mAccessibilityFocusedView = UNDEFINED;
2653                                 sendAccessibilityEventForVirtualView(virtualViewId,
2654                                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2655                                 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2656                                 return true;
2657                             }
2658                         } return false;
2659                     }
2660                 } return false;
2661                 case VIRTUAL_VIEW_ID_DECREMENT: {
2662                     switch (action) {
2663                         case AccessibilityNodeInfo.ACTION_CLICK: {
2664                             if (NumberPicker.this.isEnabled()) {
2665                                 final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
2666                                 NumberPicker.this.changeValueByOne(increment);
2667                                 sendAccessibilityEventForVirtualView(virtualViewId,
2668                                         AccessibilityEvent.TYPE_VIEW_CLICKED);
2669                                 return true;
2670                             }
2671                         } return false;
2672                         case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2673                             if (mAccessibilityFocusedView != virtualViewId) {
2674                                 mAccessibilityFocusedView = virtualViewId;
2675                                 sendAccessibilityEventForVirtualView(virtualViewId,
2676                                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2677                                 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2678                                 return true;
2679                             }
2680                         } return false;
2681                         case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2682                             if (mAccessibilityFocusedView == virtualViewId) {
2683                                 mAccessibilityFocusedView = UNDEFINED;
2684                                 sendAccessibilityEventForVirtualView(virtualViewId,
2685                                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2686                                 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2687                                 return true;
2688                             }
2689                         } return false;
2690                     }
2691                 } return false;
2692             }
2693             return super.performAction(virtualViewId, action, arguments);
2694         }
2695 
sendAccessibilityEventForVirtualView(int virtualViewId, int eventType)2696         public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) {
2697             switch (virtualViewId) {
2698                 case VIRTUAL_VIEW_ID_DECREMENT: {
2699                     if (hasVirtualDecrementButton()) {
2700                         sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
2701                                 getVirtualDecrementButtonText());
2702                     }
2703                 } break;
2704                 case VIRTUAL_VIEW_ID_INPUT: {
2705                     sendAccessibilityEventForVirtualText(eventType);
2706                 } break;
2707                 case VIRTUAL_VIEW_ID_INCREMENT: {
2708                     if (hasVirtualIncrementButton()) {
2709                         sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
2710                                 getVirtualIncrementButtonText());
2711                     }
2712                 } break;
2713             }
2714         }
2715 
sendAccessibilityEventForVirtualText(int eventType)2716         private void sendAccessibilityEventForVirtualText(int eventType) {
2717             if (AccessibilityManager.getInstance(mContext).isEnabled()) {
2718                 AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
2719                 mInputText.onInitializeAccessibilityEvent(event);
2720                 mInputText.onPopulateAccessibilityEvent(event);
2721                 event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
2722                 requestSendAccessibilityEvent(NumberPicker.this, event);
2723             }
2724         }
2725 
sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, String text)2726         private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
2727                 String text) {
2728             if (AccessibilityManager.getInstance(mContext).isEnabled()) {
2729                 AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
2730                 event.setClassName(Button.class.getName());
2731                 event.setPackageName(mContext.getPackageName());
2732                 event.getText().add(text);
2733                 event.setEnabled(NumberPicker.this.isEnabled());
2734                 event.setSource(NumberPicker.this, virtualViewId);
2735                 requestSendAccessibilityEvent(NumberPicker.this, event);
2736             }
2737         }
2738 
findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, int virtualViewId, List<AccessibilityNodeInfo> outResult)2739         private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
2740                 int virtualViewId, List<AccessibilityNodeInfo> outResult) {
2741             switch (virtualViewId) {
2742                 case VIRTUAL_VIEW_ID_DECREMENT: {
2743                     String text = getVirtualDecrementButtonText();
2744                     if (!TextUtils.isEmpty(text)
2745                             && text.toString().toLowerCase().contains(searchedLowerCase)) {
2746                         outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
2747                     }
2748                 } return;
2749                 case VIRTUAL_VIEW_ID_INPUT: {
2750                     CharSequence text = mInputText.getText();
2751                     if (!TextUtils.isEmpty(text) &&
2752                             text.toString().toLowerCase().contains(searchedLowerCase)) {
2753                         outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
2754                         return;
2755                     }
2756                     CharSequence contentDesc = mInputText.getText();
2757                     if (!TextUtils.isEmpty(contentDesc) &&
2758                             contentDesc.toString().toLowerCase().contains(searchedLowerCase)) {
2759                         outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
2760                         return;
2761                     }
2762                 } break;
2763                 case VIRTUAL_VIEW_ID_INCREMENT: {
2764                     String text = getVirtualIncrementButtonText();
2765                     if (!TextUtils.isEmpty(text)
2766                             && text.toString().toLowerCase().contains(searchedLowerCase)) {
2767                         outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
2768                     }
2769                 } return;
2770             }
2771         }
2772 
createAccessibiltyNodeInfoForInputText( int left, int top, int right, int bottom)2773         private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText(
2774                 int left, int top, int right, int bottom) {
2775             AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
2776             info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
2777             if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
2778                 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2779             }
2780             if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
2781                 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2782             }
2783             Rect boundsInParent = mTempRect;
2784             boundsInParent.set(left, top, right, bottom);
2785             info.setVisibleToUser(isVisibleToUser(boundsInParent));
2786             info.setBoundsInParent(boundsInParent);
2787             Rect boundsInScreen = boundsInParent;
2788             int[] locationOnScreen = mTempArray;
2789             getLocationOnScreen(locationOnScreen);
2790             boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2791             info.setBoundsInScreen(boundsInScreen);
2792             return info;
2793         }
2794 
createAccessibilityNodeInfoForVirtualButton(int virtualViewId, String text, int left, int top, int right, int bottom)2795         private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId,
2796                 String text, int left, int top, int right, int bottom) {
2797             AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
2798             info.setClassName(Button.class.getName());
2799             info.setPackageName(mContext.getPackageName());
2800             info.setSource(NumberPicker.this, virtualViewId);
2801             info.setParent(NumberPicker.this);
2802             info.setText(text);
2803             info.setClickable(true);
2804             info.setLongClickable(true);
2805             info.setEnabled(NumberPicker.this.isEnabled());
2806             Rect boundsInParent = mTempRect;
2807             boundsInParent.set(left, top, right, bottom);
2808             info.setVisibleToUser(isVisibleToUser(boundsInParent));
2809             info.setBoundsInParent(boundsInParent);
2810             Rect boundsInScreen = boundsInParent;
2811             int[] locationOnScreen = mTempArray;
2812             getLocationOnScreen(locationOnScreen);
2813             boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2814             info.setBoundsInScreen(boundsInScreen);
2815 
2816             if (mAccessibilityFocusedView != virtualViewId) {
2817                 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2818             }
2819             if (mAccessibilityFocusedView == virtualViewId) {
2820                 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2821             }
2822             if (NumberPicker.this.isEnabled()) {
2823                 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
2824             }
2825 
2826             return info;
2827         }
2828 
createAccessibilityNodeInfoForNumberPicker(int left, int top, int right, int bottom)2829         private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
2830                 int right, int bottom) {
2831             AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
2832             info.setClassName(NumberPicker.class.getName());
2833             info.setPackageName(mContext.getPackageName());
2834             info.setSource(NumberPicker.this);
2835 
2836             if (hasVirtualDecrementButton()) {
2837                 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
2838             }
2839             info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
2840             if (hasVirtualIncrementButton()) {
2841                 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
2842             }
2843 
2844             info.setParent((View) getParentForAccessibility());
2845             info.setEnabled(NumberPicker.this.isEnabled());
2846             info.setScrollable(true);
2847 
2848             final float applicationScale =
2849                 getContext().getResources().getCompatibilityInfo().applicationScale;
2850 
2851             Rect boundsInParent = mTempRect;
2852             boundsInParent.set(left, top, right, bottom);
2853             boundsInParent.scale(applicationScale);
2854             info.setBoundsInParent(boundsInParent);
2855 
2856             info.setVisibleToUser(isVisibleToUser());
2857 
2858             Rect boundsInScreen = boundsInParent;
2859             int[] locationOnScreen = mTempArray;
2860             getLocationOnScreen(locationOnScreen);
2861             boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2862             boundsInScreen.scale(applicationScale);
2863             info.setBoundsInScreen(boundsInScreen);
2864 
2865             if (mAccessibilityFocusedView != View.NO_ID) {
2866                 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2867             }
2868             if (mAccessibilityFocusedView == View.NO_ID) {
2869                 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2870             }
2871             if (NumberPicker.this.isEnabled()) {
2872                 if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
2873                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
2874                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
2875                 }
2876                 if (getWrapSelectorWheel() || getValue() > getMinValue()) {
2877                     info.addAction(
2878                             AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
2879                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
2880                 }
2881             }
2882 
2883             return info;
2884         }
2885 
hasVirtualDecrementButton()2886         private boolean hasVirtualDecrementButton() {
2887             return getWrapSelectorWheel() || getValue() > getMinValue();
2888         }
2889 
hasVirtualIncrementButton()2890         private boolean hasVirtualIncrementButton() {
2891             return getWrapSelectorWheel() || getValue() < getMaxValue();
2892         }
2893 
getVirtualDecrementButtonText()2894         private String getVirtualDecrementButtonText() {
2895             int value = mValue - 1;
2896             if (mWrapSelectorWheel) {
2897                 value = getWrappedSelectorIndex(value);
2898             }
2899             if (value >= mMinValue) {
2900                 return (mDisplayedValues == null) ? formatNumber(value)
2901                         : mDisplayedValues[value - mMinValue];
2902             }
2903             return null;
2904         }
2905 
getVirtualIncrementButtonText()2906         private String getVirtualIncrementButtonText() {
2907             int value = mValue + 1;
2908             if (mWrapSelectorWheel) {
2909                 value = getWrappedSelectorIndex(value);
2910             }
2911             if (value <= mMaxValue) {
2912                 return (mDisplayedValues == null) ? formatNumber(value)
2913                         : mDisplayedValues[value - mMinValue];
2914             }
2915             return null;
2916         }
2917     }
2918 
formatNumberWithLocale(int value)2919     static private String formatNumberWithLocale(int value) {
2920         return String.format(Locale.getDefault(), "%d", value);
2921     }
2922 }
2923