• 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.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.annotation.Widget;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.TypedArray;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Paint;
30 import android.graphics.Paint.Align;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
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.animation.DecelerateInterpolator;
51 import android.view.inputmethod.InputMethodManager;
52 
53 import com.android.internal.R;
54 
55 /**
56  * A widget that enables the user to select a number form a predefined range.
57  * The widget presents an input filed and up and down buttons for selecting the
58  * current value. Pressing/long pressing the up and down buttons increments and
59  * decrements the current value respectively. Touching the input filed shows a
60  * scroll wheel, tapping on which while shown and not moving allows direct edit
61  * of the current value. Sliding motions up or down hide the buttons and the
62  * input filed, show the scroll wheel, and rotate the latter. Flinging is
63  * also supported. The widget enables mapping from positions to strings such
64  * that instead the position index the corresponding string is displayed.
65  * <p>
66  * For an example of using this widget, see {@link android.widget.TimePicker}.
67  * </p>
68  */
69 @Widget
70 public class NumberPicker extends LinearLayout {
71 
72     /**
73      * The default update interval during long press.
74      */
75     private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
76 
77     /**
78      * The index of the middle selector item.
79      */
80     private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2;
81 
82     /**
83      * The coefficient by which to adjust (divide) the max fling velocity.
84      */
85     private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
86 
87     /**
88      * The the duration for adjusting the selector wheel.
89      */
90     private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
91 
92     /**
93      * The duration of scrolling to the next/previous value while changing
94      * the current value by one, i.e. increment or decrement.
95      */
96     private static final int CHANGE_CURRENT_BY_ONE_SCROLL_DURATION = 300;
97 
98     /**
99      * The the delay for showing the input controls after a single tap on the
100      * input text.
101      */
102     private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration
103             .getDoubleTapTimeout();
104 
105     /**
106      * The strength of fading in the top and bottom while drawing the selector.
107      */
108     private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
109 
110     /**
111      * The default unscaled height of the selection divider.
112      */
113     private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
114 
115     /**
116      * In this state the selector wheel is not shown.
117      */
118     private static final int SELECTOR_WHEEL_STATE_NONE = 0;
119 
120     /**
121      * In this state the selector wheel is small.
122      */
123     private static final int SELECTOR_WHEEL_STATE_SMALL = 1;
124 
125     /**
126      * In this state the selector wheel is large.
127      */
128     private static final int SELECTOR_WHEEL_STATE_LARGE = 2;
129 
130     /**
131      * The alpha of the selector wheel when it is bright.
132      */
133     private static final int SELECTOR_WHEEL_BRIGHT_ALPHA = 255;
134 
135     /**
136      * The alpha of the selector wheel when it is dimmed.
137      */
138     private static final int SELECTOR_WHEEL_DIM_ALPHA = 60;
139 
140     /**
141      * The alpha for the increment/decrement button when it is transparent.
142      */
143     private static final int BUTTON_ALPHA_TRANSPARENT = 0;
144 
145     /**
146      * The alpha for the increment/decrement button when it is opaque.
147      */
148     private static final int BUTTON_ALPHA_OPAQUE = 1;
149 
150     /**
151      * The property for setting the selector paint.
152      */
153     private static final String PROPERTY_SELECTOR_PAINT_ALPHA = "selectorPaintAlpha";
154 
155     /**
156      * The property for setting the increment/decrement button alpha.
157      */
158     private static final String PROPERTY_BUTTON_ALPHA = "alpha";
159 
160     /**
161      * The numbers accepted by the input text's {@link Filter}
162      */
163     private static final char[] DIGIT_CHARACTERS = new char[] {
164             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
165     };
166 
167     /**
168      * Use a custom NumberPicker formatting callback to use two-digit minutes
169      * strings like "01". Keeping a static formatter etc. is the most efficient
170      * way to do this; it avoids creating temporary objects on every call to
171      * format().
172      *
173      * @hide
174      */
175     public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
176         final StringBuilder mBuilder = new StringBuilder();
177 
178         final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
179 
180         final Object[] mArgs = new Object[1];
181 
182         public String format(int value) {
183             mArgs[0] = value;
184             mBuilder.delete(0, mBuilder.length());
185             mFmt.format("%02d", mArgs);
186             return mFmt.toString();
187         }
188     };
189 
190     /**
191      * The increment button.
192      */
193     private final ImageButton mIncrementButton;
194 
195     /**
196      * The decrement button.
197      */
198     private final ImageButton mDecrementButton;
199 
200     /**
201      * The text for showing the current value.
202      */
203     private final EditText mInputText;
204 
205     /**
206      * The height of the text.
207      */
208     private final int mTextSize;
209 
210     /**
211      * The height of the gap between text elements if the selector wheel.
212      */
213     private int mSelectorTextGapHeight;
214 
215     /**
216      * The values to be displayed instead the indices.
217      */
218     private String[] mDisplayedValues;
219 
220     /**
221      * Lower value of the range of numbers allowed for the NumberPicker
222      */
223     private int mMinValue;
224 
225     /**
226      * Upper value of the range of numbers allowed for the NumberPicker
227      */
228     private int mMaxValue;
229 
230     /**
231      * Current value of this NumberPicker
232      */
233     private int mValue;
234 
235     /**
236      * Listener to be notified upon current value change.
237      */
238     private OnValueChangeListener mOnValueChangeListener;
239 
240     /**
241      * Listener to be notified upon scroll state change.
242      */
243     private OnScrollListener mOnScrollListener;
244 
245     /**
246      * Formatter for for displaying the current value.
247      */
248     private Formatter mFormatter;
249 
250     /**
251      * The speed for updating the value form long press.
252      */
253     private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
254 
255     /**
256      * Cache for the string representation of selector indices.
257      */
258     private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
259 
260     /**
261      * The selector indices whose value are show by the selector.
262      */
263     private final int[] mSelectorIndices = new int[] {
264             Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE,
265             Integer.MIN_VALUE
266     };
267 
268     /**
269      * The {@link Paint} for drawing the selector.
270      */
271     private final Paint mSelectorWheelPaint;
272 
273     /**
274      * The height of a selector element (text + gap).
275      */
276     private int mSelectorElementHeight;
277 
278     /**
279      * The initial offset of the scroll selector.
280      */
281     private int mInitialScrollOffset = Integer.MIN_VALUE;
282 
283     /**
284      * The current offset of the scroll selector.
285      */
286     private int mCurrentScrollOffset;
287 
288     /**
289      * The {@link Scroller} responsible for flinging the selector.
290      */
291     private final Scroller mFlingScroller;
292 
293     /**
294      * The {@link Scroller} responsible for adjusting the selector.
295      */
296     private final Scroller mAdjustScroller;
297 
298     /**
299      * The previous Y coordinate while scrolling the selector.
300      */
301     private int mPreviousScrollerY;
302 
303     /**
304      * Handle to the reusable command for setting the input text selection.
305      */
306     private SetSelectionCommand mSetSelectionCommand;
307 
308     /**
309      * Handle to the reusable command for adjusting the scroller.
310      */
311     private AdjustScrollerCommand mAdjustScrollerCommand;
312 
313     /**
314      * Handle to the reusable command for changing the current value from long
315      * press by one.
316      */
317     private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
318 
319     /**
320      * {@link Animator} for showing the up/down arrows.
321      */
322     private final AnimatorSet mShowInputControlsAnimator;
323 
324     /**
325      * {@link Animator} for dimming the selector wheel.
326      */
327     private final Animator mDimSelectorWheelAnimator;
328 
329     /**
330      * The Y position of the last down event.
331      */
332     private float mLastDownEventY;
333 
334     /**
335      * The Y position of the last motion event.
336      */
337     private float mLastMotionEventY;
338 
339     /**
340      * Flag if to begin edit on next up event.
341      */
342     private boolean mBeginEditOnUpEvent;
343 
344     /**
345      * Flag if to adjust the selector wheel on next up event.
346      */
347     private boolean mAdjustScrollerOnUpEvent;
348 
349     /**
350      * The state of the selector wheel.
351      */
352     private int mSelectorWheelState;
353 
354     /**
355      * Determines speed during touch scrolling.
356      */
357     private VelocityTracker mVelocityTracker;
358 
359     /**
360      * @see ViewConfiguration#getScaledTouchSlop()
361      */
362     private int mTouchSlop;
363 
364     /**
365      * @see ViewConfiguration#getScaledMinimumFlingVelocity()
366      */
367     private int mMinimumFlingVelocity;
368 
369     /**
370      * @see ViewConfiguration#getScaledMaximumFlingVelocity()
371      */
372     private int mMaximumFlingVelocity;
373 
374     /**
375      * Flag whether the selector should wrap around.
376      */
377     private boolean mWrapSelectorWheel;
378 
379     /**
380      * The back ground color used to optimize scroller fading.
381      */
382     private final int mSolidColor;
383 
384     /**
385      * Flag indicating if this widget supports flinging.
386      */
387     private final boolean mFlingable;
388 
389     /**
390      * Divider for showing item to be selected while scrolling
391      */
392     private final Drawable mSelectionDivider;
393 
394     /**
395      * The height of the selection divider.
396      */
397     private final int mSelectionDividerHeight;
398 
399     /**
400      * Reusable {@link Rect} instance.
401      */
402     private final Rect mTempRect = new Rect();
403 
404     /**
405      * The current scroll state of the number picker.
406      */
407     private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
408 
409     /**
410      * The duration of the animation for showing the input controls.
411      */
412     private final long mShowInputControlsAnimimationDuration;
413 
414     /**
415      * Flag whether the scoll wheel and the fading edges have been initialized.
416      */
417     private boolean mScrollWheelAndFadingEdgesInitialized;
418 
419     /**
420      * Interface to listen for changes of the current value.
421      */
422     public interface OnValueChangeListener {
423 
424         /**
425          * Called upon a change of the current value.
426          *
427          * @param picker The NumberPicker associated with this listener.
428          * @param oldVal The previous value.
429          * @param newVal The new value.
430          */
onValueChange(NumberPicker picker, int oldVal, int newVal)431         void onValueChange(NumberPicker picker, int oldVal, int newVal);
432     }
433 
434     /**
435      * Interface to listen for the picker scroll state.
436      */
437     public interface OnScrollListener {
438 
439         /**
440          * The view is not scrolling.
441          */
442         public static int SCROLL_STATE_IDLE = 0;
443 
444         /**
445          * The user is scrolling using touch, and their finger is still on the screen.
446          */
447         public static int SCROLL_STATE_TOUCH_SCROLL = 1;
448 
449         /**
450          * The user had previously been scrolling using touch and performed a fling.
451          */
452         public static int SCROLL_STATE_FLING = 2;
453 
454         /**
455          * Callback invoked while the number picker scroll state has changed.
456          *
457          * @param view The view whose scroll state is being reported.
458          * @param scrollState The current scroll state. One of
459          *            {@link #SCROLL_STATE_IDLE},
460          *            {@link #SCROLL_STATE_TOUCH_SCROLL} or
461          *            {@link #SCROLL_STATE_IDLE}.
462          */
onScrollStateChange(NumberPicker view, int scrollState)463         public void onScrollStateChange(NumberPicker view, int scrollState);
464     }
465 
466     /**
467      * Interface used to format current value into a string for presentation.
468      */
469     public interface Formatter {
470 
471         /**
472          * Formats a string representation of the current value.
473          *
474          * @param value The currently selected value.
475          * @return A formatted string representation.
476          */
format(int value)477         public String format(int value);
478     }
479 
480     /**
481      * Create a new number picker.
482      *
483      * @param context The application environment.
484      */
NumberPicker(Context context)485     public NumberPicker(Context context) {
486         this(context, null);
487     }
488 
489     /**
490      * Create a new number picker.
491      *
492      * @param context The application environment.
493      * @param attrs A collection of attributes.
494      */
NumberPicker(Context context, AttributeSet attrs)495     public NumberPicker(Context context, AttributeSet attrs) {
496         this(context, attrs, R.attr.numberPickerStyle);
497     }
498 
499     /**
500      * Create a new number picker
501      *
502      * @param context the application environment.
503      * @param attrs a collection of attributes.
504      * @param defStyle The default style to apply to this view.
505      */
NumberPicker(Context context, AttributeSet attrs, int defStyle)506     public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
507         super(context, attrs, defStyle);
508 
509         // process style attributes
510         TypedArray attributesArray = context.obtainStyledAttributes(attrs,
511                 R.styleable.NumberPicker, defStyle, 0);
512         mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
513         mFlingable = attributesArray.getBoolean(R.styleable.NumberPicker_flingable, true);
514         mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider);
515         int defSelectionDividerHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
516                 UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
517                 getResources().getDisplayMetrics());
518         mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
519                 R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight);
520         attributesArray.recycle();
521 
522         mShowInputControlsAnimimationDuration = getResources().getInteger(
523                 R.integer.config_longAnimTime);
524 
525         // By default Linearlayout that we extend is not drawn. This is
526         // its draw() method is not called but dispatchDraw() is called
527         // directly (see ViewGroup.drawChild()). However, this class uses
528         // the fading edge effect implemented by View and we need our
529         // draw() method to be called. Therefore, we declare we will draw.
530         setWillNotDraw(false);
531         setSelectorWheelState(SELECTOR_WHEEL_STATE_NONE);
532 
533         LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
534                 Context.LAYOUT_INFLATER_SERVICE);
535         inflater.inflate(R.layout.number_picker, this, true);
536 
537         OnClickListener onClickListener = new OnClickListener() {
538             public void onClick(View v) {
539                 InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
540                 if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
541                     inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
542                 }
543                 mInputText.clearFocus();
544                 if (v.getId() == R.id.increment) {
545                     changeCurrentByOne(true);
546                 } else {
547                     changeCurrentByOne(false);
548                 }
549             }
550         };
551 
552         OnLongClickListener onLongClickListener = new OnLongClickListener() {
553             public boolean onLongClick(View v) {
554                 mInputText.clearFocus();
555                 if (v.getId() == R.id.increment) {
556                     postChangeCurrentByOneFromLongPress(true);
557                 } else {
558                     postChangeCurrentByOneFromLongPress(false);
559                 }
560                 return true;
561             }
562         };
563 
564         // increment button
565         mIncrementButton = (ImageButton) findViewById(R.id.increment);
566         mIncrementButton.setOnClickListener(onClickListener);
567         mIncrementButton.setOnLongClickListener(onLongClickListener);
568 
569         // decrement button
570         mDecrementButton = (ImageButton) findViewById(R.id.decrement);
571         mDecrementButton.setOnClickListener(onClickListener);
572         mDecrementButton.setOnLongClickListener(onLongClickListener);
573 
574         // input text
575         mInputText = (EditText) findViewById(R.id.numberpicker_input);
576         mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
577             public void onFocusChange(View v, boolean hasFocus) {
578                 if (hasFocus) {
579                     mInputText.selectAll();
580                     InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
581                     if (inputMethodManager != null) {
582                         inputMethodManager.showSoftInput(mInputText, 0);
583                     }
584                 } else {
585                     mInputText.setSelection(0, 0);
586                     validateInputTextView(v);
587                 }
588             }
589         });
590         mInputText.setFilters(new InputFilter[] {
591             new InputTextFilter()
592         });
593 
594         mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
595 
596         // initialize constants
597         mTouchSlop = ViewConfiguration.getTapTimeout();
598         ViewConfiguration configuration = ViewConfiguration.get(context);
599         mTouchSlop = configuration.getScaledTouchSlop();
600         mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
601         mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
602                 / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
603         mTextSize = (int) mInputText.getTextSize();
604 
605         // create the selector wheel paint
606         Paint paint = new Paint();
607         paint.setAntiAlias(true);
608         paint.setTextAlign(Align.CENTER);
609         paint.setTextSize(mTextSize);
610         paint.setTypeface(mInputText.getTypeface());
611         ColorStateList colors = mInputText.getTextColors();
612         int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
613         paint.setColor(color);
614         mSelectorWheelPaint = paint;
615 
616         // create the animator for showing the input controls
617         mDimSelectorWheelAnimator = ObjectAnimator.ofInt(this, PROPERTY_SELECTOR_PAINT_ALPHA,
618                 SELECTOR_WHEEL_BRIGHT_ALPHA, SELECTOR_WHEEL_DIM_ALPHA);
619         final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton,
620                 PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE);
621         final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton,
622                 PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE);
623         mShowInputControlsAnimator = new AnimatorSet();
624         mShowInputControlsAnimator.playTogether(mDimSelectorWheelAnimator, showIncrementButton,
625                 showDecrementButton);
626         mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() {
627             private boolean mCanceled = false;
628 
629             @Override
630             public void onAnimationEnd(Animator animation) {
631                 if (!mCanceled) {
632                     // if canceled => we still want the wheel drawn
633                     setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL);
634                 }
635                 mCanceled = false;
636             }
637 
638             @Override
639             public void onAnimationCancel(Animator animation) {
640                 if (mShowInputControlsAnimator.isRunning()) {
641                     mCanceled = true;
642                 }
643             }
644         });
645 
646         // create the fling and adjust scrollers
647         mFlingScroller = new Scroller(getContext(), null, true);
648         mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
649 
650         updateInputTextView();
651         updateIncrementAndDecrementButtonsVisibilityState();
652 
653         if (mFlingable) {
654            if (isInEditMode()) {
655                setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL);
656            } else {
657                 // Start with shown selector wheel and hidden controls. When made
658                 // visible hide the selector and fade-in the controls to suggest
659                 // fling interaction.
660                 setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
661                 hideInputControls();
662            }
663         }
664     }
665 
666     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)667     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
668         super.onLayout(changed, left, top, right, bottom);
669         if (!mScrollWheelAndFadingEdgesInitialized) {
670             mScrollWheelAndFadingEdgesInitialized = true;
671             // need to do all this when we know our size
672             initializeSelectorWheel();
673             initializeFadingEdges();
674         }
675     }
676 
677     @Override
onInterceptTouchEvent(MotionEvent event)678     public boolean onInterceptTouchEvent(MotionEvent event) {
679         if (!isEnabled() || !mFlingable) {
680             return false;
681         }
682         switch (event.getActionMasked()) {
683             case MotionEvent.ACTION_DOWN:
684                 mLastMotionEventY = mLastDownEventY = event.getY();
685                 removeAllCallbacks();
686                 mShowInputControlsAnimator.cancel();
687                 mDimSelectorWheelAnimator.cancel();
688                 mBeginEditOnUpEvent = false;
689                 mAdjustScrollerOnUpEvent = true;
690                 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
691                     boolean scrollersFinished = mFlingScroller.isFinished()
692                             && mAdjustScroller.isFinished();
693                     if (!scrollersFinished) {
694                         mFlingScroller.forceFinished(true);
695                         mAdjustScroller.forceFinished(true);
696                         onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
697                     }
698                     mBeginEditOnUpEvent = scrollersFinished;
699                     mAdjustScrollerOnUpEvent = true;
700                     hideInputControls();
701                     return true;
702                 }
703                 if (isEventInViewHitRect(event, mInputText)
704                         || (!mIncrementButton.isShown()
705                                 && isEventInViewHitRect(event, mIncrementButton))
706                         || (!mDecrementButton.isShown()
707                                 && isEventInViewHitRect(event, mDecrementButton))) {
708                     mAdjustScrollerOnUpEvent = false;
709                     setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
710                     hideInputControls();
711                     return true;
712                 }
713                 break;
714             case MotionEvent.ACTION_MOVE:
715                 float currentMoveY = event.getY();
716                 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
717                 if (deltaDownY > mTouchSlop) {
718                     mBeginEditOnUpEvent = false;
719                     onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
720                     setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
721                     hideInputControls();
722                     return true;
723                 }
724                 break;
725         }
726         return false;
727     }
728 
729     @Override
onTouchEvent(MotionEvent ev)730     public boolean onTouchEvent(MotionEvent ev) {
731         if (!isEnabled()) {
732             return false;
733         }
734         if (mVelocityTracker == null) {
735             mVelocityTracker = VelocityTracker.obtain();
736         }
737         mVelocityTracker.addMovement(ev);
738         int action = ev.getActionMasked();
739         switch (action) {
740             case MotionEvent.ACTION_MOVE:
741                 float currentMoveY = ev.getY();
742                 if (mBeginEditOnUpEvent
743                         || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
744                     int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
745                     if (deltaDownY > mTouchSlop) {
746                         mBeginEditOnUpEvent = false;
747                         onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
748                     }
749                 }
750                 int deltaMoveY = (int) (currentMoveY - mLastMotionEventY);
751                 scrollBy(0, deltaMoveY);
752                 invalidate();
753                 mLastMotionEventY = currentMoveY;
754                 break;
755             case MotionEvent.ACTION_UP:
756                 if (mBeginEditOnUpEvent) {
757                     setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL);
758                     showInputControls(mShowInputControlsAnimimationDuration);
759                     mInputText.requestFocus();
760                     return true;
761                 }
762                 VelocityTracker velocityTracker = mVelocityTracker;
763                 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
764                 int initialVelocity = (int) velocityTracker.getYVelocity();
765                 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
766                     fling(initialVelocity);
767                     onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
768                 } else {
769                     if (mAdjustScrollerOnUpEvent) {
770                         if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) {
771                             postAdjustScrollerCommand(0);
772                         }
773                     } else {
774                         postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS);
775                     }
776                 }
777                 mVelocityTracker.recycle();
778                 mVelocityTracker = null;
779                 break;
780         }
781         return true;
782     }
783 
784     @Override
dispatchTouchEvent(MotionEvent event)785     public boolean dispatchTouchEvent(MotionEvent event) {
786         final int action = event.getActionMasked();
787         switch (action) {
788             case MotionEvent.ACTION_MOVE:
789                 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
790                     removeAllCallbacks();
791                     forceCompleteChangeCurrentByOneViaScroll();
792                 }
793                 break;
794             case MotionEvent.ACTION_CANCEL:
795             case MotionEvent.ACTION_UP:
796                 removeAllCallbacks();
797                 break;
798         }
799         return super.dispatchTouchEvent(event);
800     }
801 
802     @Override
dispatchKeyEvent(KeyEvent event)803     public boolean dispatchKeyEvent(KeyEvent event) {
804         int keyCode = event.getKeyCode();
805         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
806             removeAllCallbacks();
807         }
808         return super.dispatchKeyEvent(event);
809     }
810 
811     @Override
dispatchTrackballEvent(MotionEvent event)812     public boolean dispatchTrackballEvent(MotionEvent event) {
813         int action = event.getActionMasked();
814         if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
815             removeAllCallbacks();
816         }
817         return super.dispatchTrackballEvent(event);
818     }
819 
820     @Override
computeScroll()821     public void computeScroll() {
822         if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) {
823             return;
824         }
825         Scroller scroller = mFlingScroller;
826         if (scroller.isFinished()) {
827             scroller = mAdjustScroller;
828             if (scroller.isFinished()) {
829                 return;
830             }
831         }
832         scroller.computeScrollOffset();
833         int currentScrollerY = scroller.getCurrY();
834         if (mPreviousScrollerY == 0) {
835             mPreviousScrollerY = scroller.getStartY();
836         }
837         scrollBy(0, currentScrollerY - mPreviousScrollerY);
838         mPreviousScrollerY = currentScrollerY;
839         if (scroller.isFinished()) {
840             onScrollerFinished(scroller);
841         } else {
842             invalidate();
843         }
844     }
845 
846     @Override
setEnabled(boolean enabled)847     public void setEnabled(boolean enabled) {
848         super.setEnabled(enabled);
849         mIncrementButton.setEnabled(enabled);
850         mDecrementButton.setEnabled(enabled);
851         mInputText.setEnabled(enabled);
852     }
853 
854     @Override
scrollBy(int x, int y)855     public void scrollBy(int x, int y) {
856         if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) {
857             return;
858         }
859         int[] selectorIndices = mSelectorIndices;
860         if (!mWrapSelectorWheel && y > 0
861                 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
862             mCurrentScrollOffset = mInitialScrollOffset;
863             return;
864         }
865         if (!mWrapSelectorWheel && y < 0
866                 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
867             mCurrentScrollOffset = mInitialScrollOffset;
868             return;
869         }
870         mCurrentScrollOffset += y;
871         while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
872             mCurrentScrollOffset -= mSelectorElementHeight;
873             decrementSelectorIndices(selectorIndices);
874             changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
875             if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
876                 mCurrentScrollOffset = mInitialScrollOffset;
877             }
878         }
879         while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
880             mCurrentScrollOffset += mSelectorElementHeight;
881             incrementSelectorIndices(selectorIndices);
882             changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
883             if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
884                 mCurrentScrollOffset = mInitialScrollOffset;
885             }
886         }
887     }
888 
889     @Override
getSolidColor()890     public int getSolidColor() {
891         return mSolidColor;
892     }
893 
894     /**
895      * Sets the listener to be notified on change of the current value.
896      *
897      * @param onValueChangedListener The listener.
898      */
setOnValueChangedListener(OnValueChangeListener onValueChangedListener)899     public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
900         mOnValueChangeListener = onValueChangedListener;
901     }
902 
903     /**
904      * Set listener to be notified for scroll state changes.
905      *
906      * @param onScrollListener The listener.
907      */
setOnScrollListener(OnScrollListener onScrollListener)908     public void setOnScrollListener(OnScrollListener onScrollListener) {
909         mOnScrollListener = onScrollListener;
910     }
911 
912     /**
913      * Set the formatter to be used for formatting the current value.
914      * <p>
915      * Note: If you have provided alternative values for the values this
916      * formatter is never invoked.
917      * </p>
918      *
919      * @param formatter The formatter object. If formatter is <code>null</code>,
920      *            {@link String#valueOf(int)} will be used.
921      *
922      * @see #setDisplayedValues(String[])
923      */
setFormatter(Formatter formatter)924     public void setFormatter(Formatter formatter) {
925         if (formatter == mFormatter) {
926             return;
927         }
928         mFormatter = formatter;
929         initializeSelectorWheelIndices();
930         updateInputTextView();
931     }
932 
933     /**
934      * Set the current value for the number picker.
935      * <p>
936      * If the argument is less than the {@link NumberPicker#getMinValue()} and
937      * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
938      * current value is set to the {@link NumberPicker#getMinValue()} value.
939      * </p>
940      * <p>
941      * If the argument is less than the {@link NumberPicker#getMinValue()} and
942      * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
943      * current value is set to the {@link NumberPicker#getMaxValue()} value.
944      * </p>
945      * <p>
946      * If the argument is less than the {@link NumberPicker#getMaxValue()} and
947      * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
948      * current value is set to the {@link NumberPicker#getMaxValue()} value.
949      * </p>
950      * <p>
951      * If the argument is less than the {@link NumberPicker#getMaxValue()} and
952      * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
953      * current value is set to the {@link NumberPicker#getMinValue()} value.
954      * </p>
955      *
956      * @param value The current value.
957      * @see #setWrapSelectorWheel(boolean)
958      * @see #setMinValue(int)
959      * @see #setMaxValue(int)
960      */
setValue(int value)961     public void setValue(int value) {
962         if (mValue == value) {
963             return;
964         }
965         if (value < mMinValue) {
966             value = mWrapSelectorWheel ? mMaxValue : mMinValue;
967         }
968         if (value > mMaxValue) {
969             value = mWrapSelectorWheel ? mMinValue : mMaxValue;
970         }
971         mValue = value;
972         initializeSelectorWheelIndices();
973         updateInputTextView();
974         updateIncrementAndDecrementButtonsVisibilityState();
975         invalidate();
976     }
977 
978     /**
979      * Gets whether the selector wheel wraps when reaching the min/max value.
980      *
981      * @return True if the selector wheel wraps.
982      *
983      * @see #getMinValue()
984      * @see #getMaxValue()
985      */
getWrapSelectorWheel()986     public boolean getWrapSelectorWheel() {
987         return mWrapSelectorWheel;
988     }
989 
990     /**
991      * Sets whether the selector wheel shown during flinging/scrolling should
992      * wrap around the {@link NumberPicker#getMinValue()} and
993      * {@link NumberPicker#getMaxValue()} values.
994      * <p>
995      * By default if the range (max - min) is more than five (the number of
996      * items shown on the selector wheel) the selector wheel wrapping is
997      * enabled.
998      * </p>
999      *
1000      * @param wrapSelectorWheel Whether to wrap.
1001      */
setWrapSelectorWheel(boolean wrapSelectorWheel)1002     public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
1003         if (wrapSelectorWheel && (mMaxValue - mMinValue) < mSelectorIndices.length) {
1004             throw new IllegalStateException("Range less than selector items count.");
1005         }
1006         if (wrapSelectorWheel != mWrapSelectorWheel) {
1007             mWrapSelectorWheel = wrapSelectorWheel;
1008             updateIncrementAndDecrementButtonsVisibilityState();
1009         }
1010     }
1011 
1012     /**
1013      * Sets the speed at which the numbers be incremented and decremented when
1014      * the up and down buttons are long pressed respectively.
1015      * <p>
1016      * The default value is 300 ms.
1017      * </p>
1018      *
1019      * @param intervalMillis The speed (in milliseconds) at which the numbers
1020      *            will be incremented and decremented.
1021      */
setOnLongPressUpdateInterval(long intervalMillis)1022     public void setOnLongPressUpdateInterval(long intervalMillis) {
1023         mLongPressUpdateInterval = intervalMillis;
1024     }
1025 
1026     /**
1027      * Returns the value of the picker.
1028      *
1029      * @return The value.
1030      */
getValue()1031     public int getValue() {
1032         return mValue;
1033     }
1034 
1035     /**
1036      * Returns the min value of the picker.
1037      *
1038      * @return The min value
1039      */
getMinValue()1040     public int getMinValue() {
1041         return mMinValue;
1042     }
1043 
1044     /**
1045      * Sets the min value of the picker.
1046      *
1047      * @param minValue The min value.
1048      */
setMinValue(int minValue)1049     public void setMinValue(int minValue) {
1050         if (mMinValue == minValue) {
1051             return;
1052         }
1053         if (minValue < 0) {
1054             throw new IllegalArgumentException("minValue must be >= 0");
1055         }
1056         mMinValue = minValue;
1057         if (mMinValue > mValue) {
1058             mValue = mMinValue;
1059         }
1060         boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1061         setWrapSelectorWheel(wrapSelectorWheel);
1062         initializeSelectorWheelIndices();
1063         updateInputTextView();
1064     }
1065 
1066     /**
1067      * Returns the max value of the picker.
1068      *
1069      * @return The max value.
1070      */
getMaxValue()1071     public int getMaxValue() {
1072         return mMaxValue;
1073     }
1074 
1075     /**
1076      * Sets the max value of the picker.
1077      *
1078      * @param maxValue The max value.
1079      */
setMaxValue(int maxValue)1080     public void setMaxValue(int maxValue) {
1081         if (mMaxValue == maxValue) {
1082             return;
1083         }
1084         if (maxValue < 0) {
1085             throw new IllegalArgumentException("maxValue must be >= 0");
1086         }
1087         mMaxValue = maxValue;
1088         if (mMaxValue < mValue) {
1089             mValue = mMaxValue;
1090         }
1091         boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1092         setWrapSelectorWheel(wrapSelectorWheel);
1093         initializeSelectorWheelIndices();
1094         updateInputTextView();
1095     }
1096 
1097     /**
1098      * Gets the values to be displayed instead of string values.
1099      *
1100      * @return The displayed values.
1101      */
getDisplayedValues()1102     public String[] getDisplayedValues() {
1103         return mDisplayedValues;
1104     }
1105 
1106     /**
1107      * Sets the values to be displayed.
1108      *
1109      * @param displayedValues The displayed values.
1110      */
setDisplayedValues(String[] displayedValues)1111     public void setDisplayedValues(String[] displayedValues) {
1112         if (mDisplayedValues == displayedValues) {
1113             return;
1114         }
1115         mDisplayedValues = displayedValues;
1116         if (mDisplayedValues != null) {
1117             // Allow text entry rather than strictly numeric entry.
1118             mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
1119                     | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
1120         } else {
1121             mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1122         }
1123         updateInputTextView();
1124         initializeSelectorWheelIndices();
1125     }
1126 
1127     @Override
getTopFadingEdgeStrength()1128     protected float getTopFadingEdgeStrength() {
1129         return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1130     }
1131 
1132     @Override
getBottomFadingEdgeStrength()1133     protected float getBottomFadingEdgeStrength() {
1134         return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1135     }
1136 
1137     @Override
onAttachedToWindow()1138     protected void onAttachedToWindow() {
1139         super.onAttachedToWindow();
1140         // make sure we show the controls only the very
1141         // first time the user sees this widget
1142         if (mFlingable && !isInEditMode()) {
1143             // animate a bit slower the very first time
1144             showInputControls(mShowInputControlsAnimimationDuration * 2);
1145         }
1146     }
1147 
1148     @Override
onDetachedFromWindow()1149     protected void onDetachedFromWindow() {
1150         removeAllCallbacks();
1151     }
1152 
1153     @Override
dispatchDraw(Canvas canvas)1154     protected void dispatchDraw(Canvas canvas) {
1155         // There is a good reason for doing this. See comments in draw().
1156     }
1157 
1158     @Override
draw(Canvas canvas)1159     public void draw(Canvas canvas) {
1160         // Dispatch draw to our children only if we are not currently running
1161         // the animation for simultaneously dimming the scroll wheel and
1162         // showing in the buttons. This class takes advantage of the View
1163         // implementation of fading edges effect to draw the selector wheel.
1164         // However, in View.draw(), the fading is applied after all the children
1165         // have been drawn and we do not want this fading to be applied to the
1166         // buttons. Therefore, we draw our children after we have completed
1167         // drawing ourselves.
1168         super.draw(canvas);
1169 
1170         // Draw our children if we are not showing the selector wheel of fading
1171         // it out
1172         if (mShowInputControlsAnimator.isRunning()
1173                 || mSelectorWheelState != SELECTOR_WHEEL_STATE_LARGE) {
1174             long drawTime = getDrawingTime();
1175             for (int i = 0, count = getChildCount(); i < count; i++) {
1176                 View child = getChildAt(i);
1177                 if (!child.isShown()) {
1178                     continue;
1179                 }
1180                 drawChild(canvas, getChildAt(i), drawTime);
1181             }
1182         }
1183     }
1184 
1185     @Override
onDraw(Canvas canvas)1186     protected void onDraw(Canvas canvas) {
1187         if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) {
1188             return;
1189         }
1190 
1191         float x = (mRight - mLeft) / 2;
1192         float y = mCurrentScrollOffset;
1193 
1194         final int restoreCount = canvas.save();
1195 
1196         if (mSelectorWheelState == SELECTOR_WHEEL_STATE_SMALL) {
1197             Rect clipBounds = canvas.getClipBounds();
1198             clipBounds.inset(0, mSelectorElementHeight);
1199             canvas.clipRect(clipBounds);
1200         }
1201 
1202         // draw the selector wheel
1203         int[] selectorIndices = mSelectorIndices;
1204         for (int i = 0; i < selectorIndices.length; i++) {
1205             int selectorIndex = selectorIndices[i];
1206             String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1207             // Do not draw the middle item if input is visible since the input is shown only
1208             // if the wheel is static and it covers the middle item. Otherwise, if the user
1209             // starts editing the text via the IME he may see a dimmed version of the old
1210             // value intermixed with the new one.
1211             if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) {
1212                 canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
1213             }
1214             y += mSelectorElementHeight;
1215         }
1216 
1217         // draw the selection dividers (only if scrolling and drawable specified)
1218         if (mSelectionDivider != null) {
1219             // draw the top divider
1220             int topOfTopDivider =
1221                 (getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2;
1222             int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
1223             mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
1224             mSelectionDivider.draw(canvas);
1225 
1226             // draw the bottom divider
1227             int topOfBottomDivider =  topOfTopDivider + mSelectorElementHeight;
1228             int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight;
1229             mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
1230             mSelectionDivider.draw(canvas);
1231         }
1232 
1233         canvas.restoreToCount(restoreCount);
1234     }
1235 
1236     @Override
sendAccessibilityEvent(int eventType)1237     public void sendAccessibilityEvent(int eventType) {
1238         // Do not send accessibility events - we want the user to
1239         // perceive this widget as several controls rather as a whole.
1240     }
1241 
1242     /**
1243      * Resets the selector indices and clear the cached
1244      * string representation of these indices.
1245      */
initializeSelectorWheelIndices()1246     private void initializeSelectorWheelIndices() {
1247         mSelectorIndexToStringCache.clear();
1248         int[] selectorIdices = mSelectorIndices;
1249         int current = getValue();
1250         for (int i = 0; i < mSelectorIndices.length; i++) {
1251             int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
1252             if (mWrapSelectorWheel) {
1253                 selectorIndex = getWrappedSelectorIndex(selectorIndex);
1254             }
1255             mSelectorIndices[i] = selectorIndex;
1256             ensureCachedScrollSelectorValue(mSelectorIndices[i]);
1257         }
1258     }
1259 
1260     /**
1261      * Sets the current value of this NumberPicker, and sets mPrevious to the
1262      * previous value. If current is greater than mEnd less than mStart, the
1263      * value of mCurrent is wrapped around. Subclasses can override this to
1264      * change the wrapping behavior
1265      *
1266      * @param current the new value of the NumberPicker
1267      */
changeCurrent(int current)1268     private void changeCurrent(int current) {
1269         if (mValue == current) {
1270             return;
1271         }
1272         // Wrap around the values if we go past the start or end
1273         if (mWrapSelectorWheel) {
1274             current = getWrappedSelectorIndex(current);
1275         }
1276         int previous = mValue;
1277         setValue(current);
1278         notifyChange(previous, current);
1279     }
1280 
1281     /**
1282      * Changes the current value by one which is increment or
1283      * decrement based on the passes argument.
1284      *
1285      * @param increment True to increment, false to decrement.
1286      */
changeCurrentByOne(boolean increment)1287     private void changeCurrentByOne(boolean increment) {
1288         if (mFlingable) {
1289             mDimSelectorWheelAnimator.cancel();
1290             mInputText.setVisibility(View.INVISIBLE);
1291             mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA);
1292             mPreviousScrollerY = 0;
1293             forceCompleteChangeCurrentByOneViaScroll();
1294             if (increment) {
1295                 mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight,
1296                         CHANGE_CURRENT_BY_ONE_SCROLL_DURATION);
1297             } else {
1298                 mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight,
1299                         CHANGE_CURRENT_BY_ONE_SCROLL_DURATION);
1300             }
1301             invalidate();
1302         } else {
1303             if (increment) {
1304                 changeCurrent(mValue + 1);
1305             } else {
1306                 changeCurrent(mValue - 1);
1307             }
1308         }
1309     }
1310 
1311     /**
1312      * Ensures that if we are in the process of changing the current value
1313      * by one via scrolling the scroller gets to its final state and the
1314      * value is updated.
1315      */
forceCompleteChangeCurrentByOneViaScroll()1316     private void forceCompleteChangeCurrentByOneViaScroll() {
1317         Scroller scroller = mFlingScroller;
1318         if (!scroller.isFinished()) {
1319             final int yBeforeAbort = scroller.getCurrY();
1320             scroller.abortAnimation();
1321             final int yDelta = scroller.getCurrY() - yBeforeAbort;
1322             scrollBy(0, yDelta);
1323         }
1324     }
1325 
1326     /**
1327      * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
1328      * wheel.
1329      */
1330     @SuppressWarnings("unused")
1331     // Called via reflection
setSelectorPaintAlpha(int alpha)1332     private void setSelectorPaintAlpha(int alpha) {
1333         mSelectorWheelPaint.setAlpha(alpha);
1334         invalidate();
1335     }
1336 
1337     /**
1338      * @return If the <code>event</code> is in the <code>view</code>.
1339      */
isEventInViewHitRect(MotionEvent event, View view)1340     private boolean isEventInViewHitRect(MotionEvent event, View view) {
1341         view.getHitRect(mTempRect);
1342         return mTempRect.contains((int) event.getX(), (int) event.getY());
1343     }
1344 
1345     /**
1346      * Sets the <code>selectorWheelState</code>.
1347      */
setSelectorWheelState(int selectorWheelState)1348     private void setSelectorWheelState(int selectorWheelState) {
1349         mSelectorWheelState = selectorWheelState;
1350         if (selectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
1351             mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA);
1352         }
1353 
1354         if (mFlingable && selectorWheelState == SELECTOR_WHEEL_STATE_LARGE
1355                 && AccessibilityManager.getInstance(mContext).isEnabled()) {
1356             AccessibilityManager.getInstance(mContext).interrupt();
1357             String text = mContext.getString(R.string.number_picker_increment_scroll_action);
1358             mInputText.setContentDescription(text);
1359             mInputText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1360             mInputText.setContentDescription(null);
1361         }
1362     }
1363 
initializeSelectorWheel()1364     private void initializeSelectorWheel() {
1365         initializeSelectorWheelIndices();
1366         int[] selectorIndices = mSelectorIndices;
1367         int totalTextHeight = selectorIndices.length * mTextSize;
1368         float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
1369         float textGapCount = selectorIndices.length - 1;
1370         mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
1371         mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
1372         // Ensure that the middle item is positioned the same as the text in mInputText
1373         int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
1374         mInitialScrollOffset = editTextTextPosition -
1375                 (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
1376         mCurrentScrollOffset = mInitialScrollOffset;
1377         updateInputTextView();
1378     }
1379 
initializeFadingEdges()1380     private void initializeFadingEdges() {
1381         setVerticalFadingEdgeEnabled(true);
1382         setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
1383     }
1384 
1385     /**
1386      * Callback invoked upon completion of a given <code>scroller</code>.
1387      */
onScrollerFinished(Scroller scroller)1388     private void onScrollerFinished(Scroller scroller) {
1389         if (scroller == mFlingScroller) {
1390             if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
1391                 postAdjustScrollerCommand(0);
1392                 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1393             } else {
1394                 updateInputTextView();
1395                 fadeSelectorWheel(mShowInputControlsAnimimationDuration);
1396             }
1397         } else {
1398             updateInputTextView();
1399             showInputControls(mShowInputControlsAnimimationDuration);
1400         }
1401     }
1402 
1403     /**
1404      * Handles transition to a given <code>scrollState</code>
1405      */
onScrollStateChange(int scrollState)1406     private void onScrollStateChange(int scrollState) {
1407         if (mScrollState == scrollState) {
1408             return;
1409         }
1410         mScrollState = scrollState;
1411         if (mOnScrollListener != null) {
1412             mOnScrollListener.onScrollStateChange(this, scrollState);
1413         }
1414     }
1415 
1416     /**
1417      * Flings the selector with the given <code>velocityY</code>.
1418      */
fling(int velocityY)1419     private void fling(int velocityY) {
1420         mPreviousScrollerY = 0;
1421         Scroller flingScroller = mFlingScroller;
1422 
1423         if (mWrapSelectorWheel) {
1424             if (velocityY > 0) {
1425                 flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1426             } else {
1427                 flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1428             }
1429         } else {
1430             if (velocityY > 0) {
1431                 int maxY = mTextSize * (mValue - mMinValue);
1432                 flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY);
1433             } else {
1434                 int startY = mTextSize * (mMaxValue - mValue);
1435                 int maxY = startY;
1436                 flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY);
1437             }
1438         }
1439 
1440         invalidate();
1441     }
1442 
1443     /**
1444      * Hides the input controls which is the up/down arrows and the text field.
1445      */
hideInputControls()1446     private void hideInputControls() {
1447         mShowInputControlsAnimator.cancel();
1448         mIncrementButton.setVisibility(INVISIBLE);
1449         mDecrementButton.setVisibility(INVISIBLE);
1450         mInputText.setVisibility(INVISIBLE);
1451     }
1452 
1453     /**
1454      * Show the input controls by making them visible and animating the alpha
1455      * property up/down arrows.
1456      *
1457      * @param animationDuration The duration of the animation.
1458      */
showInputControls(long animationDuration)1459     private void showInputControls(long animationDuration) {
1460         updateIncrementAndDecrementButtonsVisibilityState();
1461         mInputText.setVisibility(VISIBLE);
1462         mShowInputControlsAnimator.setDuration(animationDuration);
1463         mShowInputControlsAnimator.start();
1464     }
1465 
1466     /**
1467      * Fade the selector wheel via an animation.
1468      *
1469      * @param animationDuration The duration of the animation.
1470      */
fadeSelectorWheel(long animationDuration)1471     private void fadeSelectorWheel(long animationDuration) {
1472         mInputText.setVisibility(VISIBLE);
1473         mDimSelectorWheelAnimator.setDuration(animationDuration);
1474         mDimSelectorWheelAnimator.start();
1475     }
1476 
1477     /**
1478      * Updates the visibility state of the increment and decrement buttons.
1479      */
updateIncrementAndDecrementButtonsVisibilityState()1480     private void updateIncrementAndDecrementButtonsVisibilityState() {
1481         if (mWrapSelectorWheel || mValue < mMaxValue) {
1482             mIncrementButton.setVisibility(VISIBLE);
1483         } else {
1484             mIncrementButton.setVisibility(INVISIBLE);
1485         }
1486         if (mWrapSelectorWheel || mValue > mMinValue) {
1487             mDecrementButton.setVisibility(VISIBLE);
1488         } else {
1489             mDecrementButton.setVisibility(INVISIBLE);
1490         }
1491     }
1492 
1493     /**
1494      * @return The wrapped index <code>selectorIndex</code> value.
1495      */
getWrappedSelectorIndex(int selectorIndex)1496     private int getWrappedSelectorIndex(int selectorIndex) {
1497         if (selectorIndex > mMaxValue) {
1498             return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
1499         } else if (selectorIndex < mMinValue) {
1500             return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
1501         }
1502         return selectorIndex;
1503     }
1504 
1505     /**
1506      * Increments the <code>selectorIndices</code> whose string representations
1507      * will be displayed in the selector.
1508      */
incrementSelectorIndices(int[] selectorIndices)1509     private void incrementSelectorIndices(int[] selectorIndices) {
1510         for (int i = 0; i < selectorIndices.length - 1; i++) {
1511             selectorIndices[i] = selectorIndices[i + 1];
1512         }
1513         int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1514         if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
1515             nextScrollSelectorIndex = mMinValue;
1516         }
1517         selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1518         ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1519     }
1520 
1521     /**
1522      * Decrements the <code>selectorIndices</code> whose string representations
1523      * will be displayed in the selector.
1524      */
decrementSelectorIndices(int[] selectorIndices)1525     private void decrementSelectorIndices(int[] selectorIndices) {
1526         for (int i = selectorIndices.length - 1; i > 0; i--) {
1527             selectorIndices[i] = selectorIndices[i - 1];
1528         }
1529         int nextScrollSelectorIndex = selectorIndices[1] - 1;
1530         if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
1531             nextScrollSelectorIndex = mMaxValue;
1532         }
1533         selectorIndices[0] = nextScrollSelectorIndex;
1534         ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1535     }
1536 
1537     /**
1538      * Ensures we have a cached string representation of the given <code>
1539      * selectorIndex</code>
1540      * to avoid multiple instantiations of the same string.
1541      */
ensureCachedScrollSelectorValue(int selectorIndex)1542     private void ensureCachedScrollSelectorValue(int selectorIndex) {
1543         SparseArray<String> cache = mSelectorIndexToStringCache;
1544         String scrollSelectorValue = cache.get(selectorIndex);
1545         if (scrollSelectorValue != null) {
1546             return;
1547         }
1548         if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
1549             scrollSelectorValue = "";
1550         } else {
1551             if (mDisplayedValues != null) {
1552                 int displayedValueIndex = selectorIndex - mMinValue;
1553                 scrollSelectorValue = mDisplayedValues[displayedValueIndex];
1554             } else {
1555                 scrollSelectorValue = formatNumber(selectorIndex);
1556             }
1557         }
1558         cache.put(selectorIndex, scrollSelectorValue);
1559     }
1560 
formatNumber(int value)1561     private String formatNumber(int value) {
1562         return (mFormatter != null) ? mFormatter.format(value) : String.valueOf(value);
1563     }
1564 
validateInputTextView(View v)1565     private void validateInputTextView(View v) {
1566         String str = String.valueOf(((TextView) v).getText());
1567         if (TextUtils.isEmpty(str)) {
1568             // Restore to the old value as we don't allow empty values
1569             updateInputTextView();
1570         } else {
1571             // Check the new value and ensure it's in range
1572             int current = getSelectedPos(str.toString());
1573             changeCurrent(current);
1574         }
1575     }
1576 
1577     /**
1578      * Updates the view of this NumberPicker. If displayValues were specified in
1579      * the string corresponding to the index specified by the current value will
1580      * be returned. Otherwise, the formatter specified in {@link #setFormatter}
1581      * will be used to format the number.
1582      */
updateInputTextView()1583     private void updateInputTextView() {
1584         /*
1585          * If we don't have displayed values then use the current number else
1586          * find the correct value in the displayed values for the current
1587          * number.
1588          */
1589         if (mDisplayedValues == null) {
1590             mInputText.setText(formatNumber(mValue));
1591         } else {
1592             mInputText.setText(mDisplayedValues[mValue - mMinValue]);
1593         }
1594         mInputText.setSelection(mInputText.getText().length());
1595 
1596         if (mFlingable && AccessibilityManager.getInstance(mContext).isEnabled()) {
1597             String text = mContext.getString(R.string.number_picker_increment_scroll_mode,
1598                     mInputText.getText());
1599             mInputText.setContentDescription(text);
1600         }
1601     }
1602 
1603     /**
1604      * Notifies the listener, if registered, of a change of the value of this
1605      * NumberPicker.
1606      */
notifyChange(int previous, int current)1607     private void notifyChange(int previous, int current) {
1608         if (mOnValueChangeListener != null) {
1609             mOnValueChangeListener.onValueChange(this, previous, mValue);
1610         }
1611     }
1612 
1613     /**
1614      * Posts a command for changing the current value by one.
1615      *
1616      * @param increment Whether to increment or decrement the value.
1617      */
postChangeCurrentByOneFromLongPress(boolean increment)1618     private void postChangeCurrentByOneFromLongPress(boolean increment) {
1619         mInputText.clearFocus();
1620         removeAllCallbacks();
1621         if (mChangeCurrentByOneFromLongPressCommand == null) {
1622             mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
1623         }
1624         mChangeCurrentByOneFromLongPressCommand.setIncrement(increment);
1625         post(mChangeCurrentByOneFromLongPressCommand);
1626     }
1627 
1628     /**
1629      * Removes all pending callback from the message queue.
1630      */
removeAllCallbacks()1631     private void removeAllCallbacks() {
1632         if (mChangeCurrentByOneFromLongPressCommand != null) {
1633             removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1634         }
1635         if (mAdjustScrollerCommand != null) {
1636             removeCallbacks(mAdjustScrollerCommand);
1637         }
1638         if (mSetSelectionCommand != null) {
1639             removeCallbacks(mSetSelectionCommand);
1640         }
1641     }
1642 
1643     /**
1644      * @return The selected index given its displayed <code>value</code>.
1645      */
getSelectedPos(String value)1646     private int getSelectedPos(String value) {
1647         if (mDisplayedValues == null) {
1648             try {
1649                 return Integer.parseInt(value);
1650             } catch (NumberFormatException e) {
1651                 // Ignore as if it's not a number we don't care
1652             }
1653         } else {
1654             for (int i = 0; i < mDisplayedValues.length; i++) {
1655                 // Don't force the user to type in jan when ja will do
1656                 value = value.toLowerCase();
1657                 if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1658                     return mMinValue + i;
1659                 }
1660             }
1661 
1662             /*
1663              * The user might have typed in a number into the month field i.e.
1664              * 10 instead of OCT so support that too.
1665              */
1666             try {
1667                 return Integer.parseInt(value);
1668             } catch (NumberFormatException e) {
1669 
1670                 // Ignore as if it's not a number we don't care
1671             }
1672         }
1673         return mMinValue;
1674     }
1675 
1676     /**
1677      * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1678      * </code> to
1679      * <code>selectionEnd</code>.
1680      */
postSetSelectionCommand(int selectionStart, int selectionEnd)1681     private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1682         if (mSetSelectionCommand == null) {
1683             mSetSelectionCommand = new SetSelectionCommand();
1684         } else {
1685             removeCallbacks(mSetSelectionCommand);
1686         }
1687         mSetSelectionCommand.mSelectionStart = selectionStart;
1688         mSetSelectionCommand.mSelectionEnd = selectionEnd;
1689         post(mSetSelectionCommand);
1690     }
1691 
1692     /**
1693      * Posts an {@link AdjustScrollerCommand} within the given <code>
1694      * delayMillis</code>
1695      * .
1696      */
postAdjustScrollerCommand(int delayMillis)1697     private void postAdjustScrollerCommand(int delayMillis) {
1698         if (mAdjustScrollerCommand == null) {
1699             mAdjustScrollerCommand = new AdjustScrollerCommand();
1700         } else {
1701             removeCallbacks(mAdjustScrollerCommand);
1702         }
1703         postDelayed(mAdjustScrollerCommand, delayMillis);
1704     }
1705 
1706     /**
1707      * Filter for accepting only valid indices or prefixes of the string
1708      * representation of valid indices.
1709      */
1710     class InputTextFilter extends NumberKeyListener {
1711 
1712         // XXX This doesn't allow for range limits when controlled by a
1713         // soft input method!
getInputType()1714         public int getInputType() {
1715             return InputType.TYPE_CLASS_TEXT;
1716         }
1717 
1718         @Override
getAcceptedChars()1719         protected char[] getAcceptedChars() {
1720             return DIGIT_CHARACTERS;
1721         }
1722 
1723         @Override
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)1724         public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
1725                 int dstart, int dend) {
1726             if (mDisplayedValues == null) {
1727                 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1728                 if (filtered == null) {
1729                     filtered = source.subSequence(start, end);
1730                 }
1731 
1732                 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1733                         + dest.subSequence(dend, dest.length());
1734 
1735                 if ("".equals(result)) {
1736                     return result;
1737                 }
1738                 int val = getSelectedPos(result);
1739 
1740                 /*
1741                  * Ensure the user can't type in a value greater than the max
1742                  * allowed. We have to allow less than min as the user might
1743                  * want to delete some numbers and then type a new number.
1744                  */
1745                 if (val > mMaxValue) {
1746                     return "";
1747                 } else {
1748                     return filtered;
1749                 }
1750             } else {
1751                 CharSequence filtered = String.valueOf(source.subSequence(start, end));
1752                 if (TextUtils.isEmpty(filtered)) {
1753                     return "";
1754                 }
1755                 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1756                         + dest.subSequence(dend, dest.length());
1757                 String str = String.valueOf(result).toLowerCase();
1758                 for (String val : mDisplayedValues) {
1759                     String valLowerCase = val.toLowerCase();
1760                     if (valLowerCase.startsWith(str)) {
1761                         postSetSelectionCommand(result.length(), val.length());
1762                         return val.subSequence(dstart, val.length());
1763                     }
1764                 }
1765                 return "";
1766             }
1767         }
1768     }
1769 
1770     /**
1771      * Command for setting the input text selection.
1772      */
1773     class SetSelectionCommand implements Runnable {
1774         private int mSelectionStart;
1775 
1776         private int mSelectionEnd;
1777 
run()1778         public void run() {
1779             mInputText.setSelection(mSelectionStart, mSelectionEnd);
1780         }
1781     }
1782 
1783     /**
1784      * Command for adjusting the scroller to show in its center the closest of
1785      * the displayed items.
1786      */
1787     class AdjustScrollerCommand implements Runnable {
run()1788         public void run() {
1789             mPreviousScrollerY = 0;
1790             if (mInitialScrollOffset == mCurrentScrollOffset) {
1791                 updateInputTextView();
1792                 showInputControls(mShowInputControlsAnimimationDuration);
1793                 return;
1794             }
1795             // adjust to the closest value
1796             int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
1797             if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
1798                 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
1799             }
1800             mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
1801             invalidate();
1802         }
1803     }
1804 
1805     /**
1806      * Command for changing the current value from a long press by one.
1807      */
1808     class ChangeCurrentByOneFromLongPressCommand implements Runnable {
1809         private boolean mIncrement;
1810 
setIncrement(boolean increment)1811         private void setIncrement(boolean increment) {
1812             mIncrement = increment;
1813         }
1814 
run()1815         public void run() {
1816             changeCurrentByOne(mIncrement);
1817             postDelayed(this, mLongPressUpdateInterval);
1818         }
1819     }
1820 }
1821