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