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