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