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