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