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