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