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