content: /*
   * Copyright (C) 2008 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.widget;
  
  import android.annotation.CallSuper;
  import android.annotation.ColorInt;
  import android.annotation.FloatRange;
  import android.annotation.IntDef;
  import android.annotation.IntRange;
  import android.annotation.Px;
  import android.annotation.TestApi;
  import android.annotation.UnsupportedAppUsage;
  import android.annotation.Widget;
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.graphics.Paint.Align;
  import android.graphics.Rect;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.os.Bundle;
  import android.text.InputFilter;
  import android.text.InputType;
  import android.text.Spanned;
  import android.text.TextUtils;
  import android.text.method.NumberKeyListener;
  import android.util.AttributeSet;
  import android.util.SparseArray;
  import android.util.TypedValue;
  import android.view.KeyEvent;
  import android.view.LayoutInflater;
  import android.view.LayoutInflater.Filter;
  import android.view.MotionEvent;
  import android.view.VelocityTracker;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityManager;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.view.accessibility.AccessibilityNodeProvider;
  import android.view.animation.DecelerateInterpolator;
  import android.view.inputmethod.EditorInfo;
  import android.view.inputmethod.InputMethodManager;
  
  import com.android.internal.R;
  
  import libcore.icu.LocaleData;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Locale;
  
  /**
   * A widget that enables the user to select a number from a predefined range.
   * There are two flavors of this widget and which one is presented to the user
   * depends on the current theme.
   * <ul>
   * <li>
   * If the current theme is derived from {@link android.R.style#Theme} the widget
   * presents the current value as an editable input field with an increment button
   * above and a decrement button below. Long pressing the buttons allows for a quick
   * change of the current value. Tapping on the input field allows to type in
   * a desired value.
   * </li>
   * <li>
   * If the current theme is derived from {@link android.R.style#Theme_Holo} or
   * {@link android.R.style#Theme_Holo_Light} the widget presents the current
   * value as an editable input field with a lesser value above and a greater
   * value below. Tapping on the lesser or greater value selects it by animating
   * the number axis up or down to make the chosen value current. Flinging up
   * or down allows for multiple increments or decrements of the current value.
   * Long pressing on the lesser and greater values also allows for a quick change
   * of the current value. Tapping on the current value allows to type in a
   * desired value.
   * </li>
   * <li>
   * If the current theme is derived from {@link android.R.style#Theme_Material}
   * the widget presents the current value as a scrolling vertical selector with
   * the selected value in the center and the previous and following numbers above
   * and below, separated by a divider. The value is changed by flinging vertically.
   * The thickness of the divider can be changed by using the
   * {@link android.R.attr#selectionDividerHeight} attribute and the color of the
   * divider can be changed by using the
   * {@link android.R.attr#colorControlNormal} attribute.
   * </li>
   * </ul>
   * <p>
   * For an example of using this widget, see {@link android.widget.TimePicker}.
   * </p>
   */
  @Widget
  public class NumberPicker extends LinearLayout {
  
      /**
       * The number of items show in the selector wheel.
       */
      @UnsupportedAppUsage
      private static final int SELECTOR_WHEEL_ITEM_COUNT = 3;
  
      /**
       * The default update interval during long press.
       */
      private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
  
      /**
       * The index of the middle selector item.
       */
      @UnsupportedAppUsage
      private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
  
      /**
       * The coefficient by which to adjust (divide) the max fling velocity.
       */
      private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
  
      /**
       * The the duration for adjusting the selector wheel.
       */
      private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
  
      /**
       * The duration of scrolling while snapping to a given position.
       */
      private static final int SNAP_SCROLL_DURATION = 300;
  
      /**
       * The strength of fading in the top and bottom while drawing the selector.
       */
      private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
  
      /**
       * The default unscaled height of the selection divider.
       */
      private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
  
      /**
       * The default unscaled distance between the selection dividers.
       */
      private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
  
      /**
       * The resource id for the default layout.
       */
      private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker;
  
      /**
       * Constant for unspecified size.
       */
      private static final int SIZE_UNSPECIFIED = -1;
  
      /**
       * User choice on whether the selector wheel should be wrapped.
       */
      private boolean mWrapSelectorWheelPreferred = true;
  
      /**
       * Use a custom NumberPicker formatting callback to use two-digit minutes
       * strings like "01". Keeping a static formatter etc. is the most efficient
       * way to do this; it avoids creating temporary objects on every call to
       * format().
       */
      private static class TwoDigitFormatter implements NumberPicker.Formatter {
          final StringBuilder mBuilder = new StringBuilder();
  
          char mZeroDigit;
          java.util.Formatter mFmt;
  
          final Object[] mArgs = new Object[1];
  
          TwoDigitFormatter() {
              final Locale locale = Locale.getDefault();
              init(locale);
          }
  
          private void init(Locale locale) {
              mFmt = createFormatter(locale);
              mZeroDigit = getZeroDigit(locale);
          }
  
          public String format(int value) {
              final Locale currentLocale = Locale.getDefault();
              if (mZeroDigit != getZeroDigit(currentLocale)) {
                  init(currentLocale);
              }
              mArgs[0] = value;
              mBuilder.delete(0, mBuilder.length());
              mFmt.format("%02d", mArgs);
              return mFmt.toString();
          }
  
          private static char getZeroDigit(Locale locale) {
              return LocaleData.get(locale).zeroDigit;
          }
  
          private java.util.Formatter createFormatter(Locale locale) {
              return new java.util.Formatter(mBuilder, locale);
          }
      }
  
      private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter();
  
      /**
       * @hide
       */
      @UnsupportedAppUsage
      public static final Formatter getTwoDigitFormatter() {
          return sTwoDigitFormatter;
      }
  
      /**
       * The increment button.
       */
      private final ImageButton mIncrementButton;
  
      /**
       * The decrement button.
       */
      private final ImageButton mDecrementButton;
  
      /**
       * The text for showing the current value.
       */
      @UnsupportedAppUsage
      private final EditText mInputText;
  
      /**
       * The distance between the two selection dividers.
       */
      private final int mSelectionDividersDistance;
  
      /**
       * The min height of this widget.
       */
      @UnsupportedAppUsage
      private final int mMinHeight;
  
      /**
       * The max height of this widget.
       */
      private final int mMaxHeight;
  
      /**
       * The max width of this widget.
       */
      @UnsupportedAppUsage
      private final int mMinWidth;
  
      /**
       * The max width of this widget.
       */
      private int mMaxWidth;
  
      /**
       * Flag whether to compute the max width.
       */
      private final boolean mComputeMaxWidth;
  
      /**
       * The height of the text.
       */
      @UnsupportedAppUsage
      private final int mTextSize;
  
      /**
       * The height of the gap between text elements if the selector wheel.
       */
      private int mSelectorTextGapHeight;
  
      /**
       * The values to be displayed instead the indices.
       */
      private String[] mDisplayedValues;
  
      /**
       * Lower value of the range of numbers allowed for the NumberPicker
       */
      private int mMinValue;
  
      /**
       * Upper value of the range of numbers allowed for the NumberPicker
       */
      @UnsupportedAppUsage
      private int mMaxValue;
  
      /**
       * Current value of this NumberPicker
       */
      private int mValue;
  
      /**
       * Listener to be notified upon current value change.
       */
      @UnsupportedAppUsage
      private OnValueChangeListener mOnValueChangeListener;
  
      /**
       * Listener to be notified upon scroll state change.
       */
      private OnScrollListener mOnScrollListener;
  
      /**
       * Formatter for for displaying the current value.
       */
      private Formatter mFormatter;
  
      /**
       * The speed for updating the value form long press.
       */
      private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
  
      /**
       * Cache for the string representation of selector indices.
       */
      private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
  
      /**
       * The selector indices whose value are show by the selector.
       */
      @UnsupportedAppUsage
      private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
  
      /**
       * The {@link Paint} for drawing the selector.
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
      private final Paint mSelectorWheelPaint;
  
      /**
       * The {@link Drawable} for pressed virtual (increment/decrement) buttons.
       */
      private final Drawable mVirtualButtonPressedDrawable;
  
      /**
       * The height of a selector element (text + gap).
       */
      private int mSelectorElementHeight;
  
      /**
       * The initial offset of the scroll selector.
       */
      private int mInitialScrollOffset = Integer.MIN_VALUE;
  
      /**
       * The current offset of the scroll selector.
       */
      private int mCurrentScrollOffset;
  
      /**
       * The {@link Scroller} responsible for flinging the selector.
       */
      @UnsupportedAppUsage
      private final Scroller mFlingScroller;
  
      /**
       * The {@link Scroller} responsible for adjusting the selector.
       */
      private final Scroller mAdjustScroller;
  
      /**
       * The previous Y coordinate while scrolling the selector.
       */
      private int mPreviousScrollerY;
  
      /**
       * Handle to the reusable command for setting the input text selection.
       */
      private SetSelectionCommand mSetSelectionCommand;
  
      /**
       * Handle to the reusable command for changing the current value from long
       * press by one.
       */
      private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
  
      /**
       * Command for beginning an edit of the current value via IME on long press.
       */
      private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
  
      /**
       * The Y position of the last down event.
       */
      private float mLastDownEventY;
  
      /**
       * The time of the last down event.
       */
      private long mLastDownEventTime;
  
      /**
       * The Y position of the last down or move event.
       */
      private float mLastDownOrMoveEventY;
  
      /**
       * Determines speed during touch scrolling.
       */
      private VelocityTracker mVelocityTracker;
  
      /**
       * @see ViewConfiguration#getScaledTouchSlop()
       */
      private int mTouchSlop;
  
      /**
       * @see ViewConfiguration#getScaledMinimumFlingVelocity()
       */
      private int mMinimumFlingVelocity;
  
      /**
       * @see ViewConfiguration#getScaledMaximumFlingVelocity()
       */
      @UnsupportedAppUsage
      private int mMaximumFlingVelocity;
  
      /**
       * Flag whether the selector should wrap around.
       */
      private boolean mWrapSelectorWheel;
  
      /**
       * The back ground color used to optimize scroller fading.
       */
      private final int mSolidColor;
  
      /**
       * Flag whether this widget has a selector wheel.
       */
      private final boolean mHasSelectorWheel;
  
      /**
       * Divider for showing item to be selected while scrolling
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
      private final Drawable mSelectionDivider;
  
      /**
       * The height of the selection divider.
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
      private int mSelectionDividerHeight;
  
      /**
       * The current scroll state of the number picker.
       */
      private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
  
      /**
       * Flag whether to ignore move events - we ignore such when we show in IME
       * to prevent the content from scrolling.
       */
      private boolean mIgnoreMoveEvents;
  
      /**
       * Flag whether to perform a click on tap.
       */
      private boolean mPerformClickOnTap;
  
      /**
       * The top of the top selection divider.
       */
      private int mTopSelectionDividerTop;
  
      /**
       * The bottom of the bottom selection divider.
       */
      private int mBottomSelectionDividerBottom;
  
      /**
       * The virtual id of the last hovered child.
       */
      private int mLastHoveredChildVirtualViewId;
  
      /**
       * Whether the increment virtual button is pressed.
       */
      private boolean mIncrementVirtualButtonPressed;
  
      /**
       * Whether the decrement virtual button is pressed.
       */
      private boolean mDecrementVirtualButtonPressed;
  
      /**
       * Provider to report to clients the semantic structure of this widget.
       */
      private AccessibilityNodeProviderImpl mAccessibilityNodeProvider;
  
      /**
       * Helper class for managing pressed state of the virtual buttons.
       */
      private final PressedStateHelper mPressedStateHelper;
  
      /**
       * The keycode of the last handled DPAD down event.
       */
      private int mLastHandledDownDpadKeyCode = -1;
  
      /**
       * If true then the selector wheel is hidden until the picker has focus.
       */
      private boolean mHideWheelUntilFocused;
  
      /**
       * Interface to listen for changes of the current value.
       */
      public interface OnValueChangeListener {
  
          /**
           * Called upon a change of the current value.
           *
           * @param picker The NumberPicker associated with this listener.
           * @param oldVal The previous value.
           * @param newVal The new value.
           */
          void onValueChange(NumberPicker picker, int oldVal, int newVal);
      }
  
      /**
       * Interface to listen for the picker scroll state.
       */
      public interface OnScrollListener {
          /** @hide */
          @IntDef(prefix = { "SCROLL_STATE_" }, value = {
                  SCROLL_STATE_IDLE,
                  SCROLL_STATE_TOUCH_SCROLL,
                  SCROLL_STATE_FLING
          })
          @Retention(RetentionPolicy.SOURCE)
          public @interface ScrollState {}
  
          /**
           * The view is not scrolling.
           */
          public static int SCROLL_STATE_IDLE = 0;
  
          /**
           * The user is scrolling using touch, and his finger is still on the screen.
           */
          public static int SCROLL_STATE_TOUCH_SCROLL = 1;
  
          /**
           * The user had previously been scrolling using touch and performed a fling.
           */
          public static int SCROLL_STATE_FLING = 2;
  
          /**
           * Callback invoked while the number picker scroll state has changed.
           *
           * @param view The view whose scroll state is being reported.
           * @param scrollState The current scroll state. One of
           *            {@link #SCROLL_STATE_IDLE},
           *            {@link #SCROLL_STATE_TOUCH_SCROLL} or
           *            {@link #SCROLL_STATE_IDLE}.
           */
          public void onScrollStateChange(NumberPicker view, @ScrollState int scrollState);
      }
  
      /**
       * Interface used to format current value into a string for presentation.
       */
      public interface Formatter {
  
          /**
           * Formats a string representation of the current value.
           *
           * @param value The currently selected value.
           * @return A formatted string representation.
           */
          public String format(int value);
      }
  
      /**
       * Create a new number picker.
       *
       * @param context The application environment.
       */
      public NumberPicker(Context context) {
          this(context, null);
      }
  
      /**
       * Create a new number picker.
       *
       * @param context The application environment.
       * @param attrs A collection of attributes.
       */
      public NumberPicker(Context context, AttributeSet attrs) {
          this(context, attrs, R.attr.numberPickerStyle);
      }
  
      /**
       * Create a new number picker
       *
       * @param context the application environment.
       * @param attrs a collection of attributes.
       * @param defStyleAttr An attribute in the current theme that contains a
       *        reference to a style resource that supplies default values for
       *        the view. Can be 0 to not look for defaults.
       */
      public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      /**
       * Create a new number picker
       *
       * @param context the application environment.
       * @param attrs a collection of attributes.
       * @param defStyleAttr An attribute in the current theme that contains a
       *        reference to a style resource that supplies default values for
       *        the view. Can be 0 to not look for defaults.
       * @param defStyleRes A resource identifier of a style resource that
       *        supplies default values for the view, used only if
       *        defStyleAttr is 0 or can not be found in the theme. Can be 0
       *        to not look for defaults.
       */
      public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          // process style attributes
          final TypedArray attributesArray = context.obtainStyledAttributes(
                  attrs, R.styleable.NumberPicker, defStyleAttr, defStyleRes);
          saveAttributeDataForStyleable(context, R.styleable.NumberPicker,
                  attrs, attributesArray, defStyleAttr, defStyleRes);
          final int layoutResId = attributesArray.getResourceId(
                  R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID);
  
          mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
  
          mHideWheelUntilFocused = attributesArray.getBoolean(
              R.styleable.NumberPicker_hideWheelUntilFocused, false);
  
          mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
  
          final Drawable selectionDivider = attributesArray.getDrawable(
                  R.styleable.NumberPicker_selectionDivider);
          if (selectionDivider != null) {
              selectionDivider.setCallback(this);
              selectionDivider.setLayoutDirection(getLayoutDirection());
              if (selectionDivider.isStateful()) {
                  selectionDivider.setState(getDrawableState());
              }
          }
          mSelectionDivider = selectionDivider;
  
          final int defSelectionDividerHeight = (int) TypedValue.applyDimension(
                  TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
                  getResources().getDisplayMetrics());
          mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
                  R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight);
  
          final int defSelectionDividerDistance = (int) TypedValue.applyDimension(
                  TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE,
                  getResources().getDisplayMetrics());
          mSelectionDividersDistance = attributesArray.getDimensionPixelSize(
                  R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance);
  
          mMinHeight = attributesArray.getDimensionPixelSize(
                  R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED);
  
          mMaxHeight = attributesArray.getDimensionPixelSize(
                  R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED);
          if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
                  && mMinHeight > mMaxHeight) {
              throw new IllegalArgumentException("minHeight > maxHeight");
          }
  
          mMinWidth = attributesArray.getDimensionPixelSize(
                  R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED);
  
          mMaxWidth = attributesArray.getDimensionPixelSize(
                  R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED);
          if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
                  && mMinWidth > mMaxWidth) {
              throw new IllegalArgumentException("minWidth > maxWidth");
          }
  
          mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
  
          mVirtualButtonPressedDrawable = attributesArray.getDrawable(
                  R.styleable.NumberPicker_virtualButtonPressedDrawable);
  
          attributesArray.recycle();
  
          mPressedStateHelper = new PressedStateHelper();
  
          // By default Linearlayout that we extend is not drawn. This is
          // its draw() method is not called but dispatchDraw() is called
          // directly (see ViewGroup.drawChild()). However, this class uses
          // the fading edge effect implemented by View and we need our
          // draw() method to be called. Therefore, we declare we will draw.
          setWillNotDraw(!mHasSelectorWheel);
  
          LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
                  Context.LAYOUT_INFLATER_SERVICE);
          inflater.inflate(layoutResId, this, true);
  
          OnClickListener onClickListener = new OnClickListener() {
              public void onClick(View v) {
                  hideSoftInput();
                  mInputText.clearFocus();
                  if (v.getId() == R.id.increment) {
                      changeValueByOne(true);
                  } else {
                      changeValueByOne(false);
                  }
              }
          };
  
          OnLongClickListener onLongClickListener = new OnLongClickListener() {
              public boolean onLongClick(View v) {
                  hideSoftInput();
                  mInputText.clearFocus();
                  if (v.getId() == R.id.increment) {
                      postChangeCurrentByOneFromLongPress(true, 0);
                  } else {
                      postChangeCurrentByOneFromLongPress(false, 0);
                  }
                  return true;
              }
          };
  
          // increment button
          if (!mHasSelectorWheel) {
              mIncrementButton = findViewById(R.id.increment);
              mIncrementButton.setOnClickListener(onClickListener);
              mIncrementButton.setOnLongClickListener(onLongClickListener);
          } else {
              mIncrementButton = null;
          }
  
          // decrement button
          if (!mHasSelectorWheel) {
              mDecrementButton = findViewById(R.id.decrement);
              mDecrementButton.setOnClickListener(onClickListener);
              mDecrementButton.setOnLongClickListener(onLongClickListener);
          } else {
              mDecrementButton = null;
          }
  
          // input text
          mInputText = findViewById(R.id.numberpicker_input);
          mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
              public void onFocusChange(View v, boolean hasFocus) {
                  if (hasFocus) {
                      mInputText.selectAll();
                  } else {
                      mInputText.setSelection(0, 0);
                      validateInputTextView(v);
                  }
              }
          });
          mInputText.setFilters(new InputFilter[] {
              new InputTextFilter()
          });
          mInputText.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
  
          mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
          mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
  
          // initialize constants
          ViewConfiguration configuration = ViewConfiguration.get(context);
          mTouchSlop = configuration.getScaledTouchSlop();
          mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
          mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
                  / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
          mTextSize = (int) mInputText.getTextSize();
  
          // create the selector wheel paint
          Paint paint = new Paint();
          paint.setAntiAlias(true);
          paint.setTextAlign(Align.CENTER);
          paint.setTextSize(mTextSize);
          paint.setTypeface(mInputText.getTypeface());
          ColorStateList colors = mInputText.getTextColors();
          int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
          paint.setColor(color);
          mSelectorWheelPaint = paint;
  
          // create the fling and adjust scrollers
          mFlingScroller = new Scroller(getContext(), null, true);
          mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
  
          updateInputTextView();
  
          // If not explicitly specified this view is important for accessibility.
          if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
              setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
          }
  
          // Should be focusable by default, as the text view whose visibility changes is focusable
          if (getFocusable() == View.FOCUSABLE_AUTO) {
              setFocusable(View.FOCUSABLE);
              setFocusableInTouchMode(true);
          }
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          if (!mHasSelectorWheel) {
              super.onLayout(changed, left, top, right, bottom);
              return;
          }
          final int msrdWdth = getMeasuredWidth();
          final int msrdHght = getMeasuredHeight();
  
          // Input text centered horizontally.
          final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
          final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
          final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
          final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
          final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
          final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
          mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
  
          if (changed) {
              // need to do all this when we know our size
              initializeSelectorWheel();
              initializeFadingEdges();
              mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
                      - mSelectionDividerHeight;
              mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
                      + mSelectionDividersDistance;
          }
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          if (!mHasSelectorWheel) {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
              return;
          }
          // Try greedily to fit the max width and height.
          final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
          final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
          super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
          // Flag if we are measured with width or height less than the respective min.
          final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
                  widthMeasureSpec);
          final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
                  heightMeasureSpec);
          setMeasuredDimension(widthSize, heightSize);
      }
  
      /**
       * Move to the final position of a scroller. Ensures to force finish the scroller
       * and if it is not at its final position a scroll of the selector wheel is
       * performed to fast forward to the final position.
       *
       * @param scroller The scroller to whose final position to get.
       * @return True of the a move was performed, i.e. the scroller was not in final position.
       */
      private boolean moveToFinalScrollerPosition(Scroller scroller) {
          scroller.forceFinished(true);
          int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
          int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight;
          int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
          if (overshootAdjustment != 0) {
              if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) {
                  if (overshootAdjustment > 0) {
                      overshootAdjustment -= mSelectorElementHeight;
                  } else {
                      overshootAdjustment += mSelectorElementHeight;
                  }
              }
              amountToScroll += overshootAdjustment;
              scrollBy(0, amountToScroll);
              return true;
          }
          return false;
      }
  
      @Override
      public boolean onInterceptTouchEvent(MotionEvent event) {
          if (!mHasSelectorWheel || !isEnabled()) {
              return false;
          }
          final int action = event.getActionMasked();
          switch (action) {
              case MotionEvent.ACTION_DOWN: {
                  removeAllCallbacks();
                  hideSoftInput();
                  mLastDownOrMoveEventY = mLastDownEventY = event.getY();
                  mLastDownEventTime = event.getEventTime();
                  mIgnoreMoveEvents = false;
                  mPerformClickOnTap = false;
                  // Handle pressed state before any state change.
                  if (mLastDownEventY < mTopSelectionDividerTop) {
                      if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                          mPressedStateHelper.buttonPressDelayed(
                                  PressedStateHelper.BUTTON_DECREMENT);
                      }
                  } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
                      if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                          mPressedStateHelper.buttonPressDelayed(
                                  PressedStateHelper.BUTTON_INCREMENT);
                      }
                  }
                  // Make sure we support flinging inside scrollables.
                  getParent().requestDisallowInterceptTouchEvent(true);
                  if (!mFlingScroller.isFinished()) {
                      mFlingScroller.forceFinished(true);
                      mAdjustScroller.forceFinished(true);
                      onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                  } else if (!mAdjustScroller.isFinished()) {
                      mFlingScroller.forceFinished(true);
                      mAdjustScroller.forceFinished(true);
                  } else if (mLastDownEventY < mTopSelectionDividerTop) {
                      postChangeCurrentByOneFromLongPress(
                              false, ViewConfiguration.getLongPressTimeout());
                  } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
                      postChangeCurrentByOneFromLongPress(
                              true, ViewConfiguration.getLongPressTimeout());
                  } else {
                      mPerformClickOnTap = true;
                      postBeginSoftInputOnLongPressCommand();
                  }
                  return true;
              }
          }
          return false;
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (!isEnabled() || !mHasSelectorWheel) {
              return false;
          }
          if (mVelocityTracker == null) {
              mVelocityTracker = VelocityTracker.obtain();
          }
          mVelocityTracker.addMovement(event);
          int action = event.getActionMasked();
          switch (action) {
              case MotionEvent.ACTION_MOVE: {
                  if (mIgnoreMoveEvents) {
                      break;
                  }
                  float currentMoveY = event.getY();
                  if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                      int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
                      if (deltaDownY > mTouchSlop) {
                          removeAllCallbacks();
                          onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                      }
                  } else {
                      int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
                      scrollBy(0, deltaMoveY);
                      invalidate();
                  }
                  mLastDownOrMoveEventY = currentMoveY;
              } break;
              case MotionEvent.ACTION_UP: {
                  removeBeginSoftInputCommand();
                  removeChangeCurrentByOneFromLongPress();
                  mPressedStateHelper.cancel();
                  VelocityTracker velocityTracker = mVelocityTracker;
                  velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                  int initialVelocity = (int) velocityTracker.getYVelocity();
                  if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
                      fling(initialVelocity);
                      onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                  } else {
                      int eventY = (int) event.getY();
                      int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
                      long deltaTime = event.getEventTime() - mLastDownEventTime;
                      if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
                          if (mPerformClickOnTap) {
                              mPerformClickOnTap = false;
                              performClick();
                          } else {
                              int selectorIndexOffset = (eventY / mSelectorElementHeight)
                                      - SELECTOR_MIDDLE_ITEM_INDEX;
                              if (selectorIndexOffset > 0) {
                                  changeValueByOne(true);
                                  mPressedStateHelper.buttonTapped(
                                          PressedStateHelper.BUTTON_INCREMENT);
                              } else if (selectorIndexOffset < 0) {
                                  changeValueByOne(false);
                                  mPressedStateHelper.buttonTapped(
                                          PressedStateHelper.BUTTON_DECREMENT);
                              }
                          }
                      } else {
                          ensureScrollWheelAdjusted();
                      }
                      onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                  }
                  mVelocityTracker.recycle();
                  mVelocityTracker = null;
              } break;
          }
          return true;
      }
  
      @Override
      public boolean dispatchTouchEvent(MotionEvent event) {
          final int action = event.getActionMasked();
          switch (action) {
              case MotionEvent.ACTION_CANCEL:
              case MotionEvent.ACTION_UP:
                  removeAllCallbacks();
                  break;
          }
          return super.dispatchTouchEvent(event);
      }
  
      @Override
      public boolean dispatchKeyEvent(KeyEvent event) {
          final int keyCode = event.getKeyCode();
          switch (keyCode) {
              case KeyEvent.KEYCODE_DPAD_CENTER:
              case KeyEvent.KEYCODE_ENTER:
                  removeAllCallbacks();
                  break;
              case KeyEvent.KEYCODE_DPAD_DOWN:
              case KeyEvent.KEYCODE_DPAD_UP:
                  if (!mHasSelectorWheel) {
                      break;
                  }
                  switch (event.getAction()) {
                      case KeyEvent.ACTION_DOWN:
                          if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
                                  ? getValue() < getMaxValue() : getValue() > getMinValue())) {
                              requestFocus();
                              mLastHandledDownDpadKeyCode = keyCode;
                              removeAllCallbacks();
                              if (mFlingScroller.isFinished()) {
                                  changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN);
                              }
                              return true;
                          }
                          break;
                      case KeyEvent.ACTION_UP:
                          if (mLastHandledDownDpadKeyCode == keyCode) {
                              mLastHandledDownDpadKeyCode = -1;
                              return true;
                          }
                          break;
                  }
          }
          return super.dispatchKeyEvent(event);
      }
  
      @Override
      public boolean dispatchTrackballEvent(MotionEvent event) {
          final int action = event.getActionMasked();
          switch (action) {
              case MotionEvent.ACTION_CANCEL:
              case MotionEvent.ACTION_UP:
                  removeAllCallbacks();
                  break;
          }
          return super.dispatchTrackballEvent(event);
      }
  
      @Override
      protected boolean dispatchHoverEvent(MotionEvent event) {
          if (!mHasSelectorWheel) {
              return super.dispatchHoverEvent(event);
          }
          if (AccessibilityManager.getInstance(mContext).isEnabled()) {
              final int eventY = (int) event.getY();
              final int hoveredVirtualViewId;
              if (eventY < mTopSelectionDividerTop) {
                  hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
              } else if (eventY > mBottomSelectionDividerBottom) {
                  hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
              } else {
                  hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
              }
              final int action = event.getActionMasked();
              AccessibilityNodeProviderImpl provider =
                  (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider();
              switch (action) {
                  case MotionEvent.ACTION_HOVER_ENTER: {
                      provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
                              AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                      mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
                      provider.performAction(hoveredVirtualViewId,
                              AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
                  } break;
                  case MotionEvent.ACTION_HOVER_MOVE: {
                      if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
                              && mLastHoveredChildVirtualViewId != View.NO_ID) {
                          provider.sendAccessibilityEventForVirtualView(
                                  mLastHoveredChildVirtualViewId,
                                  AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                          provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
                                  AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                          mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
                          provider.performAction(hoveredVirtualViewId,
                                  AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
                      }
                  } break;
                  case MotionEvent.ACTION_HOVER_EXIT: {
                      provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
                              AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                      mLastHoveredChildVirtualViewId = View.NO_ID;
                  } break;
              }
          }
          return false;
      }
  
      @Override
      public void computeScroll() {
          Scroller scroller = mFlingScroller;
          if (scroller.isFinished()) {
              scroller = mAdjustScroller;
              if (scroller.isFinished()) {
                  return;
              }
          }
          scroller.computeScrollOffset();
          int currentScrollerY = scroller.getCurrY();
          if (mPreviousScrollerY == 0) {
              mPreviousScrollerY = scroller.getStartY();
          }
          scrollBy(0, currentScrollerY - mPreviousScrollerY);
          mPreviousScrollerY = currentScrollerY;
          if (scroller.isFinished()) {
              onScrollerFinished(scroller);
          } else {
              invalidate();
          }
      }
  
      @Override
      public void setEnabled(boolean enabled) {
          super.setEnabled(enabled);
          if (!mHasSelectorWheel) {
              mIncrementButton.setEnabled(enabled);
          }
          if (!mHasSelectorWheel) {
              mDecrementButton.setEnabled(enabled);
          }
          mInputText.setEnabled(enabled);
      }
  
      @Override
      public void scrollBy(int x, int y) {
          int[] selectorIndices = mSelectorIndices;
          int startScrollOffset = mCurrentScrollOffset;
          if (!mWrapSelectorWheel && y > 0
                  && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
              mCurrentScrollOffset = mInitialScrollOffset;
              return;
          }
          if (!mWrapSelectorWheel && y < 0
                  && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
              mCurrentScrollOffset = mInitialScrollOffset;
              return;
          }
          mCurrentScrollOffset += y;
          while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
              mCurrentScrollOffset -= mSelectorElementHeight;
              decrementSelectorIndices(selectorIndices);
              setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
              if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
                  mCurrentScrollOffset = mInitialScrollOffset;
              }
          }
          while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
              mCurrentScrollOffset += mSelectorElementHeight;
              incrementSelectorIndices(selectorIndices);
              setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
              if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
                  mCurrentScrollOffset = mInitialScrollOffset;
              }
          }
          if (startScrollOffset != mCurrentScrollOffset) {
              onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset);
          }
      }
  
      @Override
      protected int computeVerticalScrollOffset() {
          return mCurrentScrollOffset;
      }
  
      @Override
      protected int computeVerticalScrollRange() {
          return (mMaxValue - mMinValue + 1) * mSelectorElementHeight;
      }
  
      @Override
      protected int computeVerticalScrollExtent() {
          return getHeight();
      }
  
      @Override
      public int getSolidColor() {
          return mSolidColor;
      }
  
      /**
       * Sets the listener to be notified on change of the current value.
       *
       * @param onValueChangedListener The listener.
       */
      public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
          mOnValueChangeListener = onValueChangedListener;
      }
  
      /**
       * Set listener to be notified for scroll state changes.
       *
       * @param onScrollListener The listener.
       */
      public void setOnScrollListener(OnScrollListener onScrollListener) {
          mOnScrollListener = onScrollListener;
      }
  
      /**
       * Set the formatter to be used for formatting the current value.
       * <p>
       * Note: If you have provided alternative values for the values this
       * formatter is never invoked.
       * </p>
       *
       * @param formatter The formatter object. If formatter is <code>null</code>,
       *            {@link String#valueOf(int)} will be used.
       *@see #setDisplayedValues(String[])
       */
      public void setFormatter(Formatter formatter) {
          if (formatter == mFormatter) {
              return;
          }
          mFormatter = formatter;
          initializeSelectorWheelIndices();
          updateInputTextView();
      }
  
      /**
       * Set the current value for the number picker.
       * <p>
       * If the argument is less than the {@link NumberPicker#getMinValue()} and
       * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
       * current value is set to the {@link NumberPicker#getMinValue()} value.
       * </p>
       * <p>
       * If the argument is less than the {@link NumberPicker#getMinValue()} and
       * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
       * current value is set to the {@link NumberPicker#getMaxValue()} value.
       * </p>
       * <p>
       * If the argument is more than the {@link NumberPicker#getMaxValue()} and
       * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
       * current value is set to the {@link NumberPicker#getMaxValue()} value.
       * </p>
       * <p>
       * If the argument is more than the {@link NumberPicker#getMaxValue()} and
       * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
       * current value is set to the {@link NumberPicker#getMinValue()} value.
       * </p>
       *
       * @param value The current value.
       * @see #setWrapSelectorWheel(boolean)
       * @see #setMinValue(int)
       * @see #setMaxValue(int)
       */
      public void setValue(int value) {
          setValueInternal(value, false);
      }
  
      @Override
      public boolean performClick() {
          if (!mHasSelectorWheel) {
              return super.performClick();
          } else if (!super.performClick()) {
              showSoftInput();
          }
          return true;
      }
  
      @Override
      public boolean performLongClick() {
          if (!mHasSelectorWheel) {
              return super.performLongClick();
          } else if (!super.performLongClick()) {
              showSoftInput();
              mIgnoreMoveEvents = true;
          }
          return true;
      }
  
      /**
       * Shows the soft input for its input text.
       */
      private void showSoftInput() {
          InputMethodManager inputMethodManager =
                  getContext().getSystemService(InputMethodManager.class);
          if (inputMethodManager != null) {
              if (mHasSelectorWheel) {
                  mInputText.setVisibility(View.VISIBLE);
              }
              mInputText.requestFocus();
              inputMethodManager.showSoftInput(mInputText, 0);
          }
      }
  
      /**
       * Hides the soft input if it is active for the input text.
       */
      private void hideSoftInput() {
          InputMethodManager inputMethodManager =
                  getContext().getSystemService(InputMethodManager.class);
          if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
              inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
          }
          if (mHasSelectorWheel) {
              mInputText.setVisibility(View.INVISIBLE);
          }
      }
  
      /**
       * Computes the max width if no such specified as an attribute.
       */
      private void tryComputeMaxWidth() {
          if (!mComputeMaxWidth) {
              return;
          }
          int maxTextWidth = 0;
          if (mDisplayedValues == null) {
              float maxDigitWidth = 0;
              for (int i = 0; i <= 9; i++) {
                  final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
                  if (digitWidth > maxDigitWidth) {
                      maxDigitWidth = digitWidth;
                  }
              }
              int numberOfDigits = 0;
              int current = mMaxValue;
              while (current > 0) {
                  numberOfDigits++;
                  current = current / 10;
              }
              maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
          } else {
              final int valueCount = mDisplayedValues.length;
              for (int i = 0; i < valueCount; i++) {
                  final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
                  if (textWidth > maxTextWidth) {
                      maxTextWidth = (int) textWidth;
                  }
              }
          }
          maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
          if (mMaxWidth != maxTextWidth) {
              if (maxTextWidth > mMinWidth) {
                  mMaxWidth = maxTextWidth;
              } else {
                  mMaxWidth = mMinWidth;
              }
              invalidate();
          }
      }
  
      /**
       * Gets whether the selector wheel wraps when reaching the min/max value.
       *
       * @return True if the selector wheel wraps.
       *
       * @see #getMinValue()
       * @see #getMaxValue()
       */
      public boolean getWrapSelectorWheel() {
          return mWrapSelectorWheel;
      }
  
      /**
       * Sets whether the selector wheel shown during flinging/scrolling should
       * wrap around the {@link NumberPicker#getMinValue()} and
       * {@link NumberPicker#getMaxValue()} values.
       * <p>
       * By default if the range (max - min) is more than the number of items shown
       * on the selector wheel the selector wheel wrapping is enabled.
       * </p>
       * <p>
       * <strong>Note:</strong> If the number of items, i.e. the range (
       * {@link #getMaxValue()} - {@link #getMinValue()}) is less than
       * the number of items shown on the selector wheel, the selector wheel will
       * not wrap. Hence, in such a case calling this method is a NOP.
       * </p>
       *
       * @param wrapSelectorWheel Whether to wrap.
       */
      public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
          mWrapSelectorWheelPreferred = wrapSelectorWheel;
          updateWrapSelectorWheel();
  
      }
  
      /**
       * Whether or not the selector wheel should be wrapped is determined by user choice and whether
       * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the
       * latter is calculated based on min & max value set vs selector's visual length. Therefore,
       * this method should be called any time any of the 3 values (i.e. user choice, min and max
       * value) gets updated.
       */
      private void updateWrapSelectorWheel() {
          final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
          mWrapSelectorWheel = wrappingAllowed && mWrapSelectorWheelPreferred;
      }
  
      /**
       * Sets the speed at which the numbers be incremented and decremented when
       * the up and down buttons are long pressed respectively.
       * <p>
       * The default value is 300 ms.
       * </p>
       *
       * @param intervalMillis The speed (in milliseconds) at which the numbers
       *            will be incremented and decremented.
       */
      public void setOnLongPressUpdateInterval(long intervalMillis) {
          mLongPressUpdateInterval = intervalMillis;
      }
  
      /**
       * Returns the value of the picker.
       *
       * @return The value.
       */
      public int getValue() {
          return mValue;
      }
  
      /**
       * Returns the min value of the picker.
       *
       * @return The min value
       */
      public int getMinValue() {
          return mMinValue;
      }
  
      /**
       * Sets the min value of the picker.
       *
       * @param minValue The min value inclusive.
       *
       * <strong>Note:</strong> The length of the displayed values array
       * set via {@link #setDisplayedValues(String[])} must be equal to the
       * range of selectable numbers which is equal to
       * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
       */
      public void setMinValue(int minValue) {
          if (mMinValue == minValue) {
              return;
          }
          if (minValue < 0) {
              throw new IllegalArgumentException("minValue must be >= 0");
          }
          mMinValue = minValue;
          if (mMinValue > mValue) {
              mValue = mMinValue;
          }
          updateWrapSelectorWheel();
          initializeSelectorWheelIndices();
          updateInputTextView();
          tryComputeMaxWidth();
          invalidate();
      }
  
      /**
       * Returns the max value of the picker.
       *
       * @return The max value.
       */
      public int getMaxValue() {
          return mMaxValue;
      }
  
      /**
       * Sets the max value of the picker.
       *
       * @param maxValue The max value inclusive.
       *
       * <strong>Note:</strong> The length of the displayed values array
       * set via {@link #setDisplayedValues(String[])} must be equal to the
       * range of selectable numbers which is equal to
       * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
       */
      public void setMaxValue(int maxValue) {
          if (mMaxValue == maxValue) {
              return;
          }
          if (maxValue < 0) {
              throw new IllegalArgumentException("maxValue must be >= 0");
          }
          mMaxValue = maxValue;
          if (mMaxValue < mValue) {
              mValue = mMaxValue;
          }
          updateWrapSelectorWheel();
          initializeSelectorWheelIndices();
          updateInputTextView();
          tryComputeMaxWidth();
          invalidate();
      }
  
      /**
       * Gets the values to be displayed instead of string values.
       *
       * @return The displayed values.
       */
      public String[] getDisplayedValues() {
          return mDisplayedValues;
      }
  
      /**
       * Sets the values to be displayed.
       *
       * @param displayedValues The displayed values.
       *
       * <strong>Note:</strong> The length of the displayed values array
       * must be equal to the range of selectable numbers which is equal to
       * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
       */
      public void setDisplayedValues(String[] displayedValues) {
          if (mDisplayedValues == displayedValues) {
              return;
          }
          mDisplayedValues = displayedValues;
          if (mDisplayedValues != null) {
              // Allow text entry rather than strictly numeric entry.
              mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
                      | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
          } else {
              mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
          }
          updateInputTextView();
          initializeSelectorWheelIndices();
          tryComputeMaxWidth();
      }
  
      /**
       * Retrieves the displayed value for the current selection in this picker.
       *
       * @hide
       */
      @TestApi
      public CharSequence getDisplayedValueForCurrentSelection() {
          // The cache field itself is initialized at declaration time, and since it's final, it
          // can't be null here. The cache is updated in ensureCachedScrollSelectorValue which is
          // called, directly or indirectly, on every call to setDisplayedValues, setFormatter,
          // setMinValue, setMaxValue and setValue, as well as user-driven interaction with the
          // picker. As such, the contents of the cache are always synced to the latest state of
          // the widget.
          return mSelectorIndexToStringCache.get(getValue());
      }
  
      /**
       * Set the height for the divider that separates the currently selected value from the others.
       * @param height The height to be set
       */
      public void setSelectionDividerHeight(@IntRange(from = 0) @Px int height) {
          mSelectionDividerHeight = height;
          invalidate();
      }
  
      /**
       * Retrieve the height for the divider that separates the currently selected value from the
       * others.
       * @return The height of the divider
       */
      @Px
      public int getSelectionDividerHeight() {
          return mSelectionDividerHeight;
      }
  
      @Override
      protected float getTopFadingEdgeStrength() {
          return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
      }
  
      @Override
      protected float getBottomFadingEdgeStrength() {
          return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          removeAllCallbacks();
      }
  
      @CallSuper
      @Override
      protected void drawableStateChanged() {
          super.drawableStateChanged();
  
          final Drawable selectionDivider = mSelectionDivider;
          if (selectionDivider != null && selectionDivider.isStateful()
                  && selectionDivider.setState(getDrawableState())) {
              invalidateDrawable(selectionDivider);
          }
      }
  
      @CallSuper
      @Override
      public void jumpDrawablesToCurrentState() {
          super.jumpDrawablesToCurrentState();
  
          if (mSelectionDivider != null) {
              mSelectionDivider.jumpToCurrentState();
          }
      }
  
      /** @hide */
      @Override
      public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
          super.onResolveDrawables(layoutDirection);
  
          if (mSelectionDivider != null) {
              mSelectionDivider.setLayoutDirection(layoutDirection);
          }
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          if (!mHasSelectorWheel) {
              super.onDraw(canvas);
              return;
          }
          final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true;
          float x = (mRight - mLeft) / 2;
          float y = mCurrentScrollOffset;
  
          // draw the virtual buttons pressed state if needed
          if (showSelectorWheel && mVirtualButtonPressedDrawable != null
                  && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
              if (mDecrementVirtualButtonPressed) {
                  mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
                  mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop);
                  mVirtualButtonPressedDrawable.draw(canvas);
              }
              if (mIncrementVirtualButtonPressed) {
                  mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
                  mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight,
                          mBottom);
                  mVirtualButtonPressedDrawable.draw(canvas);
              }
          }
  
          // draw the selector wheel
          int[] selectorIndices = mSelectorIndices;
          for (int i = 0; i < selectorIndices.length; i++) {
              int selectorIndex = selectorIndices[i];
              String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
              // Do not draw the middle item if input is visible since the input
              // is shown only if the wheel is static and it covers the middle
              // item. Otherwise, if the user starts editing the text via the
              // IME he may see a dimmed version of the old value intermixed
              // with the new one.
              if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) ||
                  (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) {
                  canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
              }
              y += mSelectorElementHeight;
          }
  
          // draw the selection dividers
          if (showSelectorWheel && mSelectionDivider != null) {
              // draw the top divider
              int topOfTopDivider = mTopSelectionDividerTop;
              int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
              mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
              mSelectionDivider.draw(canvas);
  
              // draw the bottom divider
              int bottomOfBottomDivider = mBottomSelectionDividerBottom;
              int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
              mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
              mSelectionDivider.draw(canvas);
          }
      }
  
      /** @hide */
      @Override
      public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
          super.onInitializeAccessibilityEventInternal(event);
          event.setClassName(NumberPicker.class.getName());
          event.setScrollable(true);
          event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
          event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
      }
  
      @Override
      public AccessibilityNodeProvider getAccessibilityNodeProvider() {
          if (!mHasSelectorWheel) {
              return super.getAccessibilityNodeProvider();
          }
          if (mAccessibilityNodeProvider == null) {
              mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
          }
          return mAccessibilityNodeProvider;
      }
  
      /**
       * Sets the text color for all the states (normal, selected, focused) to be the given color.
       *
       * @param color A color value in the form 0xAARRGGBB.
       */
      public void setTextColor(@ColorInt int color) {
          mSelectorWheelPaint.setColor(color);
          mInputText.setTextColor(color);
          invalidate();
      }
  
      /**
       * @return the text color.
       */
      @ColorInt
      public int getTextColor() {
          return mSelectorWheelPaint.getColor();
      }
  
      /**
       * Sets the text size to the given value. This value must be > 0
       *
       * @param size The size in pixel units.
       */
      public void setTextSize(@FloatRange(from = 0.0, fromInclusive = false) float size) {
          mSelectorWheelPaint.setTextSize(size);
          mInputText.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
          invalidate();
      }
  
      /**
       * @return the size (in pixels) of the text size in this NumberPicker.
       */
      @FloatRange(from = 0.0, fromInclusive = false)
      public float getTextSize() {
          return mSelectorWheelPaint.getTextSize();
      }
  
      /**
       * Makes a measure spec that tries greedily to use the max value.
       *
       * @param measureSpec The measure spec.
       * @param maxSize The max value for the size.
       * @return A measure spec greedily imposing the max size.
       */
      private int makeMeasureSpec(int measureSpec, int maxSize) {
          if (maxSize == SIZE_UNSPECIFIED) {
              return measureSpec;
          }
          final int size = MeasureSpec.getSize(measureSpec);
          final int mode = MeasureSpec.getMode(measureSpec);
          switch (mode) {
              case MeasureSpec.EXACTLY:
                  return measureSpec;
              case MeasureSpec.AT_MOST:
                  return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
              case MeasureSpec.UNSPECIFIED:
                  return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
              default:
                  throw new IllegalArgumentException("Unknown measure mode: " + mode);
          }
      }
  
      /**
       * Utility to reconcile a desired size and state, with constraints imposed
       * by a MeasureSpec. Tries to respect the min size, unless a different size
       * is imposed by the constraints.
       *
       * @param minSize The minimal desired size.
       * @param measuredSize The currently measured size.
       * @param measureSpec The current measure spec.
       * @return The resolved size and state.
       */
      private int resolveSizeAndStateRespectingMinSize(
              int minSize, int measuredSize, int measureSpec) {
          if (minSize != SIZE_UNSPECIFIED) {
              final int desiredWidth = Math.max(minSize, measuredSize);
              return resolveSizeAndState(desiredWidth, measureSpec, 0);
          } else {
              return measuredSize;
          }
      }
  
      /**
       * Resets the selector indices and clear the cached string representation of
       * these indices.
       */
      @UnsupportedAppUsage
      private void initializeSelectorWheelIndices() {
          mSelectorIndexToStringCache.clear();
          int[] selectorIndices = mSelectorIndices;
          int current = getValue();
          for (int i = 0; i < mSelectorIndices.length; i++) {
              int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
              if (mWrapSelectorWheel) {
                  selectorIndex = getWrappedSelectorIndex(selectorIndex);
              }
              selectorIndices[i] = selectorIndex;
              ensureCachedScrollSelectorValue(selectorIndices[i]);
          }
      }
  
      /**
       * Sets the current value of this NumberPicker.
       *
       * @param current The new value of the NumberPicker.
       * @param notifyChange Whether to notify if the current value changed.
       */
      private void setValueInternal(int current, boolean notifyChange) {
          if (mValue == current) {
              return;
          }
          // Wrap around the values if we go past the start or end
          if (mWrapSelectorWheel) {
              current = getWrappedSelectorIndex(current);
          } else {
              current = Math.max(current, mMinValue);
              current = Math.min(current, mMaxValue);
          }
          int previous = mValue;
          mValue = current;
          // If we're flinging, we'll update the text view at the end when it becomes visible
          if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
              updateInputTextView();
          }
          if (notifyChange) {
              notifyChange(previous, current);
          }
          initializeSelectorWheelIndices();
          invalidate();
      }
  
      /**
       * Changes the current value by one which is increment or
       * decrement based on the passes argument.
       * decrement the current value.
       *
       * @param increment True to increment, false to decrement.
       */
       @UnsupportedAppUsage
       private void changeValueByOne(boolean increment) {
          if (mHasSelectorWheel) {
              hideSoftInput();
              if (!moveToFinalScrollerPosition(mFlingScroller)) {
                  moveToFinalScrollerPosition(mAdjustScroller);
              }
              mPreviousScrollerY = 0;
              if (increment) {
                  mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION);
              } else {
                  mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION);
              }
              invalidate();
          } else {
              if (increment) {
                  setValueInternal(mValue + 1, true);
              } else {
                  setValueInternal(mValue - 1, true);
              }
          }
      }
  
      private void initializeSelectorWheel() {
          initializeSelectorWheelIndices();
          int[] selectorIndices = mSelectorIndices;
          int totalTextHeight = selectorIndices.length * mTextSize;
          float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
          float textGapCount = selectorIndices.length;
          mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
          mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
          // Ensure that the middle item is positioned the same as the text in
          // mInputText
          int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
          mInitialScrollOffset = editTextTextPosition
                  - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
          mCurrentScrollOffset = mInitialScrollOffset;
          updateInputTextView();
      }
  
      private void initializeFadingEdges() {
          setVerticalFadingEdgeEnabled(true);
          setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
      }
  
      /**
       * Callback invoked upon completion of a given <code>scroller</code>.
       */
      private void onScrollerFinished(Scroller scroller) {
          if (scroller == mFlingScroller) {
              ensureScrollWheelAdjusted();
              updateInputTextView();
              onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
          } else {
              if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                  updateInputTextView();
              }
          }
      }
  
      /**
       * Handles transition to a given <code>scrollState</code>
       */
      private void onScrollStateChange(int scrollState) {
          if (mScrollState == scrollState) {
              return;
          }
          mScrollState = scrollState;
          if (mOnScrollListener != null) {
              mOnScrollListener.onScrollStateChange(this, scrollState);
          }
      }
  
      /**
       * Flings the selector with the given <code>velocityY</code>.
       */
      private void fling(int velocityY) {
          mPreviousScrollerY = 0;
  
          if (velocityY > 0) {
              mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
          } else {
              mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
          }
  
          invalidate();
      }
  
      /**
       * @return The wrapped index <code>selectorIndex</code> value.
       */
      private int getWrappedSelectorIndex(int selectorIndex) {
          if (selectorIndex > mMaxValue) {
              return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
          } else if (selectorIndex < mMinValue) {
              return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
          }
          return selectorIndex;
      }
  
      /**
       * Increments the <code>selectorIndices</code> whose string representations
       * will be displayed in the selector.
       */
      private void incrementSelectorIndices(int[] selectorIndices) {
          for (int i = 0; i < selectorIndices.length - 1; i++) {
              selectorIndices[i] = selectorIndices[i + 1];
          }
          int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
          if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
              nextScrollSelectorIndex = mMinValue;
          }
          selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
          ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
      }
  
      /**
       * Decrements the <code>selectorIndices</code> whose string representations
       * will be displayed in the selector.
       */
      private void decrementSelectorIndices(int[] selectorIndices) {
          for (int i = selectorIndices.length - 1; i > 0; i--) {
              selectorIndices[i] = selectorIndices[i - 1];
          }
          int nextScrollSelectorIndex = selectorIndices[1] - 1;
          if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
              nextScrollSelectorIndex = mMaxValue;
          }
          selectorIndices[0] = nextScrollSelectorIndex;
          ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
      }
  
      /**
       * Ensures we have a cached string representation of the given <code>
       * selectorIndex</code> to avoid multiple instantiations of the same string.
       */
      private void ensureCachedScrollSelectorValue(int selectorIndex) {
          SparseArray<String> cache = mSelectorIndexToStringCache;
          String scrollSelectorValue = cache.get(selectorIndex);
          if (scrollSelectorValue != null) {
              return;
          }
          if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
              scrollSelectorValue = "";
          } else {
              if (mDisplayedValues != null) {
                  int displayedValueIndex = selectorIndex - mMinValue;
                  scrollSelectorValue = mDisplayedValues[displayedValueIndex];
              } else {
                  scrollSelectorValue = formatNumber(selectorIndex);
              }
          }
          cache.put(selectorIndex, scrollSelectorValue);
      }
  
      private String formatNumber(int value) {
          return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
      }
  
      private void validateInputTextView(View v) {
          String str = String.valueOf(((TextView) v).getText());
          if (TextUtils.isEmpty(str)) {
              // Restore to the old value as we don't allow empty values
              updateInputTextView();
          } else {
              // Check the new value and ensure it's in range
              int current = getSelectedPos(str.toString());
              setValueInternal(current, true);
          }
      }
  
      /**
       * Updates the view of this NumberPicker. If displayValues were specified in
       * the string corresponding to the index specified by the current value will
       * be returned. Otherwise, the formatter specified in {@link #setFormatter}
       * will be used to format the number.
       *
       * @return Whether the text was updated.
       */
      private boolean updateInputTextView() {
          /*
           * If we don't have displayed values then use the current number else
           * find the correct value in the displayed values for the current
           * number.
           */
          String text = (mDisplayedValues == null) ? formatNumber(mValue)
                  : mDisplayedValues[mValue - mMinValue];
          if (!TextUtils.isEmpty(text)) {
              CharSequence beforeText = mInputText.getText();
              if (!text.equals(beforeText.toString())) {
                  mInputText.setText(text);
                  if (AccessibilityManager.getInstance(mContext).isEnabled()) {
                      AccessibilityEvent event = AccessibilityEvent.obtain(
                              AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
                      mInputText.onInitializeAccessibilityEvent(event);
                      mInputText.onPopulateAccessibilityEvent(event);
                      event.setFromIndex(0);
                      event.setRemovedCount(beforeText.length());
                      event.setAddedCount(text.length());
                      event.setBeforeText(beforeText);
                      event.setSource(NumberPicker.this,
                              AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT);
                      requestSendAccessibilityEvent(NumberPicker.this, event);
                  }
                  return true;
              }
          }
  
          return false;
      }
  
      /**
       * Notifies the listener, if registered, of a change of the value of this
       * NumberPicker.
       */
      private void notifyChange(int previous, int current) {
          if (mOnValueChangeListener != null) {
              mOnValueChangeListener.onValueChange(this, previous, mValue);
          }
      }
  
      /**
       * Posts a command for changing the current value by one.
       *
       * @param increment Whether to increment or decrement the value.
       */
      private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
          if (mChangeCurrentByOneFromLongPressCommand == null) {
              mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
          } else {
              removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
          }
          mChangeCurrentByOneFromLongPressCommand.setStep(increment);
          postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
      }
  
      /**
       * Removes the command for changing the current value by one.
       */
      private void removeChangeCurrentByOneFromLongPress() {
          if (mChangeCurrentByOneFromLongPressCommand != null) {
              removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
          }
      }
  
      /**
       * Posts a command for beginning an edit of the current value via IME on
       * long press.
       */
      private void postBeginSoftInputOnLongPressCommand() {
          if (mBeginSoftInputOnLongPressCommand == null) {
              mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
          } else {
              removeCallbacks(mBeginSoftInputOnLongPressCommand);
          }
          postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout());
      }
  
      /**
       * Removes the command for beginning an edit of the current value via IME.
       */
      private void removeBeginSoftInputCommand() {
          if (mBeginSoftInputOnLongPressCommand != null) {
              removeCallbacks(mBeginSoftInputOnLongPressCommand);
          }
      }
  
      /**
       * Removes all pending callback from the message queue.
       */
      private void removeAllCallbacks() {
          if (mChangeCurrentByOneFromLongPressCommand != null) {
              removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
          }
          if (mSetSelectionCommand != null) {
              mSetSelectionCommand.cancel();
          }
          if (mBeginSoftInputOnLongPressCommand != null) {
              removeCallbacks(mBeginSoftInputOnLongPressCommand);
          }
          mPressedStateHelper.cancel();
      }
  
      /**
       * @return The selected index given its displayed <code>value</code>.
       */
      private int getSelectedPos(String value) {
          if (mDisplayedValues == null) {
              try {
                  return Integer.parseInt(value);
              } catch (NumberFormatException e) {
                  // Ignore as if it's not a number we don't care
              }
          } else {
              for (int i = 0; i < mDisplayedValues.length; i++) {
                  // Don't force the user to type in jan when ja will do
                  value = value.toLowerCase();
                  if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
                      return mMinValue + i;
                  }
              }
  
              /*
               * The user might have typed in a number into the month field i.e.
               * 10 instead of OCT so support that too.
               */
              try {
                  return Integer.parseInt(value);
              } catch (NumberFormatException e) {
  
                  // Ignore as if it's not a number we don't care
              }
          }
          return mMinValue;
      }
  
      /**
       * Posts a {@link SetSelectionCommand} from the given
       * {@code selectionStart} to {@code selectionEnd}.
       */
      private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
          if (mSetSelectionCommand == null) {
              mSetSelectionCommand = new SetSelectionCommand(mInputText);
          }
          mSetSelectionCommand.post(selectionStart, selectionEnd);
      }
  
      /**
       * The numbers accepted by the input text's {@link Filter}
       */
      private static final char[] DIGIT_CHARACTERS = new char[] {
              // Latin digits are the common case
              '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
              // Arabic-Indic
              '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
              , '\u0669',
              // Extended Arabic-Indic
              '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
              , '\u06f9',
              // Hindi and Marathi (Devanagari script)
              '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e'
              , '\u096f',
              // Bengali
              '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee'
              , '\u09ef',
              // Kannada
              '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee'
              , '\u0cef'
      };
  
      /**
       * Filter for accepting only valid indices or prefixes of the string
       * representation of valid indices.
       */
      class InputTextFilter extends NumberKeyListener {
  
          // XXX This doesn't allow for range limits when controlled by a
          // soft input method!
          public int getInputType() {
              return InputType.TYPE_CLASS_TEXT;
          }
  
          @Override
          protected char[] getAcceptedChars() {
              return DIGIT_CHARACTERS;
          }
  
          @Override
          public CharSequence filter(
                  CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
              // We don't know what the output will be, so always cancel any
              // pending set selection command.
              if (mSetSelectionCommand != null) {
                  mSetSelectionCommand.cancel();
              }
  
              if (mDisplayedValues == null) {
                  CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
                  if (filtered == null) {
                      filtered = source.subSequence(start, end);
                  }
  
                  String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
                          + dest.subSequence(dend, dest.length());
  
                  if ("".equals(result)) {
                      return result;
                  }
                  int val = getSelectedPos(result);
  
                  /*
                   * Ensure the user can't type in a value greater than the max
                   * allowed. We have to allow less than min as the user might
                   * want to delete some numbers and then type a new number.
                   * And prevent multiple-"0" that exceeds the length of upper
                   * bound number.
                   */
                  if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) {
                      return "";
                  } else {
                      return filtered;
                  }
              } else {
                  CharSequence filtered = String.valueOf(source.subSequence(start, end));
                  if (TextUtils.isEmpty(filtered)) {
                      return "";
                  }
                  String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
                          + dest.subSequence(dend, dest.length());
                  String str = String.valueOf(result).toLowerCase();
                  for (String val : mDisplayedValues) {
                      String valLowerCase = val.toLowerCase();
                      if (valLowerCase.startsWith(str)) {
                          postSetSelectionCommand(result.length(), val.length());
                          return val.subSequence(dstart, val.length());
                      }
                  }
                  return "";
              }
          }
      }
  
      /**
       * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
       * middle element is in the middle of the widget.
       *
       * @return Whether an adjustment has been made.
       */
      private boolean ensureScrollWheelAdjusted() {
          // adjust to the closest value
          int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
          if (deltaY != 0) {
              mPreviousScrollerY = 0;
              if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
                  deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
              }
              mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
              invalidate();
              return true;
          }
          return false;
      }
  
      class PressedStateHelper implements Runnable {
          public static final int BUTTON_INCREMENT = 1;
          public static final int BUTTON_DECREMENT = 2;
  
          private final int MODE_PRESS = 1;
          private final int MODE_TAPPED = 2;
  
          private int mManagedButton;
          private int mMode;
  
          public void cancel() {
              mMode = 0;
              mManagedButton = 0;
              NumberPicker.this.removeCallbacks(this);
              if (mIncrementVirtualButtonPressed) {
                  mIncrementVirtualButtonPressed = false;
                  invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
              }
              mDecrementVirtualButtonPressed = false;
              if (mDecrementVirtualButtonPressed) {
                  invalidate(0, 0, mRight, mTopSelectionDividerTop);
              }
          }
  
          public void buttonPressDelayed(int button) {
              cancel();
              mMode = MODE_PRESS;
              mManagedButton = button;
              NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
          }
  
          public void buttonTapped(int button) {
              cancel();
              mMode = MODE_TAPPED;
              mManagedButton = button;
              NumberPicker.this.post(this);
          }
  
          @Override
          public void run() {
              switch (mMode) {
                  case MODE_PRESS: {
                      switch (mManagedButton) {
                          case BUTTON_INCREMENT: {
                              mIncrementVirtualButtonPressed = true;
                              invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
                          } break;
                          case BUTTON_DECREMENT: {
                              mDecrementVirtualButtonPressed = true;
                              invalidate(0, 0, mRight, mTopSelectionDividerTop);
                          }
                      }
                  } break;
                  case MODE_TAPPED: {
                      switch (mManagedButton) {
                          case BUTTON_INCREMENT: {
                              if (!mIncrementVirtualButtonPressed) {
                                  NumberPicker.this.postDelayed(this,
                                          ViewConfiguration.getPressedStateDuration());
                              }
                              mIncrementVirtualButtonPressed ^= true;
                              invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
                          } break;
                          case BUTTON_DECREMENT: {
                              if (!mDecrementVirtualButtonPressed) {
                                  NumberPicker.this.postDelayed(this,
                                          ViewConfiguration.getPressedStateDuration());
                              }
                              mDecrementVirtualButtonPressed ^= true;
                              invalidate(0, 0, mRight, mTopSelectionDividerTop);
                          }
                      }
                  } break;
              }
          }
      }
  
      /**
       * Command for setting the input text selection.
       */
      private static class SetSelectionCommand implements Runnable {
          private final EditText mInputText;
  
          private int mSelectionStart;
          private int mSelectionEnd;
  
          /** Whether this runnable is currently posted. */
          private boolean mPosted;
  
          public SetSelectionCommand(EditText inputText) {
              mInputText = inputText;
          }
  
          public void post(int selectionStart, int selectionEnd) {
              mSelectionStart = selectionStart;
              mSelectionEnd = selectionEnd;
  
              if (!mPosted) {
                  mInputText.post(this);
                  mPosted = true;
              }
          }
  
          public void cancel() {
              if (mPosted) {
                  mInputText.removeCallbacks(this);
                  mPosted = false;
              }
          }
  
          @Override
          public void run() {
              mPosted = false;
              mInputText.setSelection(mSelectionStart, mSelectionEnd);
          }
      }
  
      /**
       * Command for changing the current value from a long press by one.
       */
      class ChangeCurrentByOneFromLongPressCommand implements Runnable {
          private boolean mIncrement;
  
          private void setStep(boolean increment) {
              mIncrement = increment;
          }
  
          @Override
          public void run() {
              changeValueByOne(mIncrement);
              postDelayed(this, mLongPressUpdateInterval);
          }
      }
  
      /**
       * @hide
       */
      public static class CustomEditText extends EditText {
  
          public CustomEditText(Context context, AttributeSet attrs) {
              super(context, attrs);
          }
  
          @Override
          public void onEditorAction(int actionCode) {
              super.onEditorAction(actionCode);
              if (actionCode == EditorInfo.IME_ACTION_DONE) {
                  clearFocus();
              }
          }
      }
  
      /**
       * Command for beginning soft input on long press.
       */
      class BeginSoftInputOnLongPressCommand implements Runnable {
  
          @Override
          public void run() {
              performLongClick();
          }
      }
  
      /**
       * Class for managing virtual view tree rooted at this picker.
       */
      class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
          private static final int UNDEFINED = Integer.MIN_VALUE;
  
          private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
  
          private static final int VIRTUAL_VIEW_ID_INPUT = 2;
  
          private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
  
          private final Rect mTempRect = new Rect();
  
          private final int[] mTempArray = new int[2];
  
          private int mAccessibilityFocusedView = UNDEFINED;
  
          @Override
          public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
              switch (virtualViewId) {
                  case View.NO_ID:
                      return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
                              mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
                  case VIRTUAL_VIEW_ID_DECREMENT:
                      return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
                              getVirtualDecrementButtonText(), mScrollX, mScrollY,
                              mScrollX + (mRight - mLeft),
                              mTopSelectionDividerTop + mSelectionDividerHeight);
                  case VIRTUAL_VIEW_ID_INPUT:
                      return createAccessibiltyNodeInfoForInputText(mScrollX,
                              mTopSelectionDividerTop + mSelectionDividerHeight,
                              mScrollX + (mRight - mLeft),
                              mBottomSelectionDividerBottom - mSelectionDividerHeight);
                  case VIRTUAL_VIEW_ID_INCREMENT:
                      return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
                              getVirtualIncrementButtonText(), mScrollX,
                              mBottomSelectionDividerBottom - mSelectionDividerHeight,
                              mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
              }
              return super.createAccessibilityNodeInfo(virtualViewId);
          }
  
          @Override
          public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
                  int virtualViewId) {
              if (TextUtils.isEmpty(searched)) {
                  return Collections.emptyList();
              }
              String searchedLowerCase = searched.toLowerCase();
              List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
              switch (virtualViewId) {
                  case View.NO_ID: {
                      findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                              VIRTUAL_VIEW_ID_DECREMENT, result);
                      findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                              VIRTUAL_VIEW_ID_INPUT, result);
                      findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                              VIRTUAL_VIEW_ID_INCREMENT, result);
                      return result;
                  }
                  case VIRTUAL_VIEW_ID_DECREMENT:
                  case VIRTUAL_VIEW_ID_INCREMENT:
                  case VIRTUAL_VIEW_ID_INPUT: {
                      findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId,
                              result);
                      return result;
                  }
              }
              return super.findAccessibilityNodeInfosByText(searched, virtualViewId);
          }
  
          @Override
          public boolean performAction(int virtualViewId, int action, Bundle arguments) {
              switch (virtualViewId) {
                  case View.NO_ID: {
                      switch (action) {
                          case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView != virtualViewId) {
                                  mAccessibilityFocusedView = virtualViewId;
                                  requestAccessibilityFocus();
                                  return true;
                              }
                          } return false;
                          case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView == virtualViewId) {
                                  mAccessibilityFocusedView = UNDEFINED;
                                  clearAccessibilityFocus();
                                  return true;
                              }
                              return false;
                          }
                          case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
                              if (NumberPicker.this.isEnabled()
                                      && (getWrapSelectorWheel() || getValue() < getMaxValue())) {
                                  changeValueByOne(true);
                                  return true;
                              }
                          } return false;
                          case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
                              if (NumberPicker.this.isEnabled()
                                      && (getWrapSelectorWheel() || getValue() > getMinValue())) {
                                  changeValueByOne(false);
                                  return true;
                              }
                          } return false;
                      }
                  } break;
                  case VIRTUAL_VIEW_ID_INPUT: {
                      switch (action) {
                          case AccessibilityNodeInfo.ACTION_FOCUS: {
                              if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) {
                                  return mInputText.requestFocus();
                              }
                          } break;
                          case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
                              if (NumberPicker.this.isEnabled() && mInputText.isFocused()) {
                                  mInputText.clearFocus();
                                  return true;
                              }
                              return false;
                          }
                          case AccessibilityNodeInfo.ACTION_CLICK: {
                              if (NumberPicker.this.isEnabled()) {
                                  performClick();
                                  return true;
                              }
                              return false;
                          }
                          case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
                              if (NumberPicker.this.isEnabled()) {
                                  performLongClick();
                                  return true;
                              }
                              return false;
                          }
                          case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView != virtualViewId) {
                                  mAccessibilityFocusedView = virtualViewId;
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                  mInputText.invalidate();
                                  return true;
                              }
                          } return false;
                          case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView == virtualViewId) {
                                  mAccessibilityFocusedView = UNDEFINED;
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                                  mInputText.invalidate();
                                  return true;
                              }
                          } return false;
                          default: {
                              return mInputText.performAccessibilityAction(action, arguments);
                          }
                      }
                  } return false;
                  case VIRTUAL_VIEW_ID_INCREMENT: {
                      switch (action) {
                          case AccessibilityNodeInfo.ACTION_CLICK: {
                              if (NumberPicker.this.isEnabled()) {
                                  NumberPicker.this.changeValueByOne(true);
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_CLICKED);
                                  return true;
                              }
                          } return false;
                          case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView != virtualViewId) {
                                  mAccessibilityFocusedView = virtualViewId;
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                  invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
                                  return true;
                              }
                          } return false;
                          case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView == virtualViewId) {
                                  mAccessibilityFocusedView = UNDEFINED;
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                                  invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
                                  return true;
                              }
                          } return false;
                      }
                  } return false;
                  case VIRTUAL_VIEW_ID_DECREMENT: {
                      switch (action) {
                          case AccessibilityNodeInfo.ACTION_CLICK: {
                              if (NumberPicker.this.isEnabled()) {
                                  final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
                                  NumberPicker.this.changeValueByOne(increment);
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_CLICKED);
                                  return true;
                              }
                          } return false;
                          case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView != virtualViewId) {
                                  mAccessibilityFocusedView = virtualViewId;
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                  invalidate(0, 0, mRight, mTopSelectionDividerTop);
                                  return true;
                              }
                          } return false;
                          case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
                              if (mAccessibilityFocusedView == virtualViewId) {
                                  mAccessibilityFocusedView = UNDEFINED;
                                  sendAccessibilityEventForVirtualView(virtualViewId,
                                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                                  invalidate(0, 0, mRight, mTopSelectionDividerTop);
                                  return true;
                              }
                          } return false;
                      }
                  } return false;
              }
              return super.performAction(virtualViewId, action, arguments);
          }
  
          public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) {
              switch (virtualViewId) {
                  case VIRTUAL_VIEW_ID_DECREMENT: {
                      if (hasVirtualDecrementButton()) {
                          sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
                                  getVirtualDecrementButtonText());
                      }
                  } break;
                  case VIRTUAL_VIEW_ID_INPUT: {
                      sendAccessibilityEventForVirtualText(eventType);
                  } break;
                  case VIRTUAL_VIEW_ID_INCREMENT: {
                      if (hasVirtualIncrementButton()) {
                          sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
                                  getVirtualIncrementButtonText());
                      }
                  } break;
              }
          }
  
          private void sendAccessibilityEventForVirtualText(int eventType) {
              if (AccessibilityManager.getInstance(mContext).isEnabled()) {
                  AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
                  mInputText.onInitializeAccessibilityEvent(event);
                  mInputText.onPopulateAccessibilityEvent(event);
                  event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
                  requestSendAccessibilityEvent(NumberPicker.this, event);
              }
          }
  
          private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
                  String text) {
              if (AccessibilityManager.getInstance(mContext).isEnabled()) {
                  AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
                  event.setClassName(Button.class.getName());
                  event.setPackageName(mContext.getPackageName());
                  event.getText().add(text);
                  event.setEnabled(NumberPicker.this.isEnabled());
                  event.setSource(NumberPicker.this, virtualViewId);
                  requestSendAccessibilityEvent(NumberPicker.this, event);
              }
          }
  
          private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
                  int virtualViewId, List<AccessibilityNodeInfo> outResult) {
              switch (virtualViewId) {
                  case VIRTUAL_VIEW_ID_DECREMENT: {
                      String text = getVirtualDecrementButtonText();
                      if (!TextUtils.isEmpty(text)
                              && text.toString().toLowerCase().contains(searchedLowerCase)) {
                          outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
                      }
                  } return;
                  case VIRTUAL_VIEW_ID_INPUT: {
                      CharSequence text = mInputText.getText();
                      if (!TextUtils.isEmpty(text) &&
                              text.toString().toLowerCase().contains(searchedLowerCase)) {
                          outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
                          return;
                      }
                      CharSequence contentDesc = mInputText.getText();
                      if (!TextUtils.isEmpty(contentDesc) &&
                              contentDesc.toString().toLowerCase().contains(searchedLowerCase)) {
                          outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
                          return;
                      }
                  } break;
                  case VIRTUAL_VIEW_ID_INCREMENT: {
                      String text = getVirtualIncrementButtonText();
                      if (!TextUtils.isEmpty(text)
                              && text.toString().toLowerCase().contains(searchedLowerCase)) {
                          outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
                      }
                  } return;
              }
          }
  
          private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText(
                  int left, int top, int right, int bottom) {
              AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
              info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
              if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
                  info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
              }
              if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
                  info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
              }
              Rect boundsInParent = mTempRect;
              boundsInParent.set(left, top, right, bottom);
              info.setVisibleToUser(isVisibleToUser(boundsInParent));
              info.setBoundsInParent(boundsInParent);
              Rect boundsInScreen = boundsInParent;
              int[] locationOnScreen = mTempArray;
              getLocationOnScreen(locationOnScreen);
              boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
              info.setBoundsInScreen(boundsInScreen);
              return info;
          }
  
          private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId,
                  String text, int left, int top, int right, int bottom) {
              AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
              info.setClassName(Button.class.getName());
              info.setPackageName(mContext.getPackageName());
              info.setSource(NumberPicker.this, virtualViewId);
              info.setParent(NumberPicker.this);
              info.setText(text);
              info.setClickable(true);
              info.setLongClickable(true);
              info.setEnabled(NumberPicker.this.isEnabled());
              Rect boundsInParent = mTempRect;
              boundsInParent.set(left, top, right, bottom);
              info.setVisibleToUser(isVisibleToUser(boundsInParent));
              info.setBoundsInParent(boundsInParent);
              Rect boundsInScreen = boundsInParent;
              int[] locationOnScreen = mTempArray;
              getLocationOnScreen(locationOnScreen);
              boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
              info.setBoundsInScreen(boundsInScreen);
  
              if (mAccessibilityFocusedView != virtualViewId) {
                  info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
              }
              if (mAccessibilityFocusedView == virtualViewId) {
                  info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
              }
              if (NumberPicker.this.isEnabled()) {
                  info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
              }
  
              return info;
          }
  
          private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
                  int right, int bottom) {
              AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
              info.setClassName(NumberPicker.class.getName());
              info.setPackageName(mContext.getPackageName());
              info.setSource(NumberPicker.this);
  
              if (hasVirtualDecrementButton()) {
                  info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
              }
              info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
              if (hasVirtualIncrementButton()) {
                  info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
              }
  
              info.setParent((View) getParentForAccessibility());
              info.setEnabled(NumberPicker.this.isEnabled());
              info.setScrollable(true);
  
              final float applicationScale =
                  getContext().getResources().getCompatibilityInfo().applicationScale;
  
              Rect boundsInParent = mTempRect;
              boundsInParent.set(left, top, right, bottom);
              boundsInParent.scale(applicationScale);
              info.setBoundsInParent(boundsInParent);
  
              info.setVisibleToUser(isVisibleToUser());
  
              Rect boundsInScreen = boundsInParent;
              int[] locationOnScreen = mTempArray;
              getLocationOnScreen(locationOnScreen);
              boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
              boundsInScreen.scale(applicationScale);
              info.setBoundsInScreen(boundsInScreen);
  
              if (mAccessibilityFocusedView != View.NO_ID) {
                  info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
              }
              if (mAccessibilityFocusedView == View.NO_ID) {
                  info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
              }
              if (NumberPicker.this.isEnabled()) {
                  if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
                      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                  }
                  if (getWrapSelectorWheel() || getValue() > getMinValue()) {
                      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
                  }
              }
  
              return info;
          }
  
          private boolean hasVirtualDecrementButton() {
              return getWrapSelectorWheel() || getValue() > getMinValue();
          }
  
          private boolean hasVirtualIncrementButton() {
              return getWrapSelectorWheel() || getValue() < getMaxValue();
          }
  
          private String getVirtualDecrementButtonText() {
              int value = mValue - 1;
              if (mWrapSelectorWheel) {
                  value = getWrappedSelectorIndex(value);
              }
              if (value >= mMinValue) {
                  return (mDisplayedValues == null) ? formatNumber(value)
                          : mDisplayedValues[value - mMinValue];
              }
              return null;
          }
  
          private String getVirtualIncrementButtonText() {
              int value = mValue + 1;
              if (mWrapSelectorWheel) {
                  value = getWrappedSelectorIndex(value);
              }
              if (value <= mMaxValue) {
                  return (mDisplayedValues == null) ? formatNumber(value)
                          : mDisplayedValues[value - mMinValue];
              }
              return null;
          }
      }
  
      static private String formatNumberWithLocale(int value) {
          return String.format(Locale.getDefault(), "%d", value);
      }
  }
  