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