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