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