NumberPicker.java revision 0aa8f3b8db3eb97f53dc7fe8f09d5b983300417c
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.annotation.Widget; 24import android.content.Context; 25import android.content.res.ColorStateList; 26import android.content.res.TypedArray; 27import android.graphics.Canvas; 28import android.graphics.Color; 29import android.graphics.Paint; 30import android.graphics.Paint.Align; 31import android.graphics.Rect; 32import android.graphics.drawable.Drawable; 33import android.text.InputFilter; 34import android.text.InputType; 35import android.text.Spanned; 36import android.text.TextUtils; 37import android.text.method.NumberKeyListener; 38import android.util.AttributeSet; 39import android.util.SparseArray; 40import android.util.TypedValue; 41import android.view.KeyEvent; 42import android.view.LayoutInflater; 43import android.view.LayoutInflater.Filter; 44import android.view.MotionEvent; 45import android.view.VelocityTracker; 46import android.view.View; 47import android.view.ViewConfiguration; 48import android.view.accessibility.AccessibilityEvent; 49import android.view.accessibility.AccessibilityManager; 50import android.view.accessibility.AccessibilityNodeInfo; 51import android.view.animation.DecelerateInterpolator; 52import android.view.inputmethod.InputMethodManager; 53 54import com.android.internal.R; 55 56/** 57 * A widget that enables the user to select a number form a predefined range. 58 * The widget presents an input field and up and down buttons for selecting the 59 * current value. Pressing/long-pressing the up and down buttons increments and 60 * decrements the current value respectively. Touching the input field shows a 61 * scroll wheel, which when touched allows direct edit 62 * of the current value. Sliding gestures up or down hide the buttons and the 63 * input filed, show and rotate the scroll wheel. Flinging is 64 * also supported. The widget enables mapping from positions to strings such 65 * that, instead of the position index, the corresponding string is displayed. 66 * <p> 67 * For an example of using this widget, see {@link android.widget.TimePicker}. 68 * </p> 69 */ 70@Widget 71public class NumberPicker extends LinearLayout { 72 73 /** 74 * The number of items show in the selector wheel. 75 */ 76 public static final int SELECTOR_WHEEL_ITEM_COUNT = 5; 77 78 /** 79 * The default update interval during long press. 80 */ 81 private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; 82 83 /** 84 * The index of the middle selector item. 85 */ 86 private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2; 87 88 /** 89 * The coefficient by which to adjust (divide) the max fling velocity. 90 */ 91 private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; 92 93 /** 94 * The the duration for adjusting the selector wheel. 95 */ 96 private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; 97 98 /** 99 * The duration of scrolling to the next/previous value while changing 100 * the current value by one, i.e. increment or decrement. 101 */ 102 private static final int CHANGE_CURRENT_BY_ONE_SCROLL_DURATION = 300; 103 104 /** 105 * The the delay for showing the input controls after a single tap on the 106 * input text. 107 */ 108 private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration 109 .getDoubleTapTimeout(); 110 111 /** 112 * The strength of fading in the top and bottom while drawing the selector. 113 */ 114 private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; 115 116 /** 117 * The default unscaled height of the selection divider. 118 */ 119 private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2; 120 121 /** 122 * In this state the selector wheel is not shown. 123 */ 124 private static final int SELECTOR_WHEEL_STATE_NONE = 0; 125 126 /** 127 * In this state the selector wheel is small. 128 */ 129 private static final int SELECTOR_WHEEL_STATE_SMALL = 1; 130 131 /** 132 * In this state the selector wheel is large. 133 */ 134 private static final int SELECTOR_WHEEL_STATE_LARGE = 2; 135 136 /** 137 * The alpha of the selector wheel when it is bright. 138 */ 139 private static final int SELECTOR_WHEEL_BRIGHT_ALPHA = 255; 140 141 /** 142 * The alpha of the selector wheel when it is dimmed. 143 */ 144 private static final int SELECTOR_WHEEL_DIM_ALPHA = 60; 145 146 /** 147 * The alpha for the increment/decrement button when it is transparent. 148 */ 149 private static final int BUTTON_ALPHA_TRANSPARENT = 0; 150 151 /** 152 * The alpha for the increment/decrement button when it is opaque. 153 */ 154 private static final int BUTTON_ALPHA_OPAQUE = 1; 155 156 /** 157 * The property for setting the selector paint. 158 */ 159 private static final String PROPERTY_SELECTOR_PAINT_ALPHA = "selectorPaintAlpha"; 160 161 /** 162 * The property for setting the increment/decrement button alpha. 163 */ 164 private static final String PROPERTY_BUTTON_ALPHA = "alpha"; 165 166 /** 167 * The numbers accepted by the input text's {@link Filter} 168 */ 169 private static final char[] DIGIT_CHARACTERS = new char[] { 170 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 171 }; 172 173 /** 174 * Constant for unspecified size. 175 */ 176 private static final int SIZE_UNSPECIFIED = -1; 177 178 /** 179 * Use a custom NumberPicker formatting callback to use two-digit minutes 180 * strings like "01". Keeping a static formatter etc. is the most efficient 181 * way to do this; it avoids creating temporary objects on every call to 182 * format(). 183 * 184 * @hide 185 */ 186 public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() { 187 final StringBuilder mBuilder = new StringBuilder(); 188 189 final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US); 190 191 final Object[] mArgs = new Object[1]; 192 193 public String format(int value) { 194 mArgs[0] = value; 195 mBuilder.delete(0, mBuilder.length()); 196 mFmt.format("%02d", mArgs); 197 return mFmt.toString(); 198 } 199 }; 200 201 /** 202 * The increment button. 203 */ 204 private final ImageButton mIncrementButton; 205 206 /** 207 * The decrement button. 208 */ 209 private final ImageButton mDecrementButton; 210 211 /** 212 * The text for showing the current value. 213 */ 214 private final EditText mInputText; 215 216 /** 217 * The min height of this widget. 218 */ 219 private final int mMinHeight; 220 221 /** 222 * The max height of this widget. 223 */ 224 private final int mMaxHeight; 225 226 /** 227 * The max width of this widget. 228 */ 229 private final int mMinWidth; 230 231 /** 232 * The max width of this widget. 233 */ 234 private int mMaxWidth; 235 236 /** 237 * Flag whether to compute the max width. 238 */ 239 private final boolean mComputeMaxWidth; 240 241 /** 242 * The height of the text. 243 */ 244 private final int mTextSize; 245 246 /** 247 * The height of the gap between text elements if the selector wheel. 248 */ 249 private int mSelectorTextGapHeight; 250 251 /** 252 * The values to be displayed instead the indices. 253 */ 254 private String[] mDisplayedValues; 255 256 /** 257 * Lower value of the range of numbers allowed for the NumberPicker 258 */ 259 private int mMinValue; 260 261 /** 262 * Upper value of the range of numbers allowed for the NumberPicker 263 */ 264 private int mMaxValue; 265 266 /** 267 * Current value of this NumberPicker 268 */ 269 private int mValue; 270 271 /** 272 * Listener to be notified upon current value change. 273 */ 274 private OnValueChangeListener mOnValueChangeListener; 275 276 /** 277 * Listener to be notified upon scroll state change. 278 */ 279 private OnScrollListener mOnScrollListener; 280 281 /** 282 * Formatter for for displaying the current value. 283 */ 284 private Formatter mFormatter; 285 286 /** 287 * The speed for updating the value form long press. 288 */ 289 private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; 290 291 /** 292 * Cache for the string representation of selector indices. 293 */ 294 private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>(); 295 296 /** 297 * The selector indices whose value are show by the selector. 298 */ 299 private final int[] mSelectorIndices = new int[] { 300 Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, 301 Integer.MIN_VALUE 302 }; 303 304 /** 305 * The {@link Paint} for drawing the selector. 306 */ 307 private final Paint mSelectorWheelPaint; 308 309 /** 310 * The height of a selector element (text + gap). 311 */ 312 private int mSelectorElementHeight; 313 314 /** 315 * The initial offset of the scroll selector. 316 */ 317 private int mInitialScrollOffset = Integer.MIN_VALUE; 318 319 /** 320 * The current offset of the scroll selector. 321 */ 322 private int mCurrentScrollOffset; 323 324 /** 325 * The {@link Scroller} responsible for flinging the selector. 326 */ 327 private final Scroller mFlingScroller; 328 329 /** 330 * The {@link Scroller} responsible for adjusting the selector. 331 */ 332 private final Scroller mAdjustScroller; 333 334 /** 335 * The previous Y coordinate while scrolling the selector. 336 */ 337 private int mPreviousScrollerY; 338 339 /** 340 * Handle to the reusable command for setting the input text selection. 341 */ 342 private SetSelectionCommand mSetSelectionCommand; 343 344 /** 345 * Handle to the reusable command for adjusting the scroller. 346 */ 347 private AdjustScrollerCommand mAdjustScrollerCommand; 348 349 /** 350 * Handle to the reusable command for changing the current value from long 351 * press by one. 352 */ 353 private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand; 354 355 /** 356 * {@link Animator} for showing the up/down arrows. 357 */ 358 private final AnimatorSet mShowInputControlsAnimator; 359 360 /** 361 * {@link Animator} for dimming the selector wheel. 362 */ 363 private final Animator mDimSelectorWheelAnimator; 364 365 /** 366 * The Y position of the last down event. 367 */ 368 private float mLastDownEventY; 369 370 /** 371 * The Y position of the last motion event. 372 */ 373 private float mLastMotionEventY; 374 375 /** 376 * Flag if to begin edit on next up event. 377 */ 378 private boolean mBeginEditOnUpEvent; 379 380 /** 381 * Flag if to adjust the selector wheel on next up event. 382 */ 383 private boolean mAdjustScrollerOnUpEvent; 384 385 /** 386 * The state of the selector wheel. 387 */ 388 private int mSelectorWheelState; 389 390 /** 391 * Determines speed during touch scrolling. 392 */ 393 private VelocityTracker mVelocityTracker; 394 395 /** 396 * @see ViewConfiguration#getScaledTouchSlop() 397 */ 398 private int mTouchSlop; 399 400 /** 401 * @see ViewConfiguration#getScaledMinimumFlingVelocity() 402 */ 403 private int mMinimumFlingVelocity; 404 405 /** 406 * @see ViewConfiguration#getScaledMaximumFlingVelocity() 407 */ 408 private int mMaximumFlingVelocity; 409 410 /** 411 * Flag whether the selector should wrap around. 412 */ 413 private boolean mWrapSelectorWheel; 414 415 /** 416 * The back ground color used to optimize scroller fading. 417 */ 418 private final int mSolidColor; 419 420 /** 421 * Flag indicating if this widget supports flinging. 422 */ 423 private final boolean mFlingable; 424 425 /** 426 * Divider for showing item to be selected while scrolling 427 */ 428 private final Drawable mSelectionDivider; 429 430 /** 431 * The height of the selection divider. 432 */ 433 private final int mSelectionDividerHeight; 434 435 /** 436 * Reusable {@link Rect} instance. 437 */ 438 private final Rect mTempRect = new Rect(); 439 440 /** 441 * The current scroll state of the number picker. 442 */ 443 private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; 444 445 /** 446 * The duration of the animation for showing the input controls. 447 */ 448 private final long mShowInputControlsAnimimationDuration; 449 450 /** 451 * Flag whether the scoll wheel and the fading edges have been initialized. 452 */ 453 private boolean mScrollWheelAndFadingEdgesInitialized; 454 455 /** 456 * Interface to listen for changes of the current value. 457 */ 458 public interface OnValueChangeListener { 459 460 /** 461 * Called upon a change of the current value. 462 * 463 * @param picker The NumberPicker associated with this listener. 464 * @param oldVal The previous value. 465 * @param newVal The new value. 466 */ 467 void onValueChange(NumberPicker picker, int oldVal, int newVal); 468 } 469 470 /** 471 * Interface to listen for the picker scroll state. 472 */ 473 public interface OnScrollListener { 474 475 /** 476 * The view is not scrolling. 477 */ 478 public static int SCROLL_STATE_IDLE = 0; 479 480 /** 481 * The user is scrolling using touch, and their finger is still on the screen. 482 */ 483 public static int SCROLL_STATE_TOUCH_SCROLL = 1; 484 485 /** 486 * The user had previously been scrolling using touch and performed a fling. 487 */ 488 public static int SCROLL_STATE_FLING = 2; 489 490 /** 491 * Callback invoked while the number picker scroll state has changed. 492 * 493 * @param view The view whose scroll state is being reported. 494 * @param scrollState The current scroll state. One of 495 * {@link #SCROLL_STATE_IDLE}, 496 * {@link #SCROLL_STATE_TOUCH_SCROLL} or 497 * {@link #SCROLL_STATE_IDLE}. 498 */ 499 public void onScrollStateChange(NumberPicker view, int scrollState); 500 } 501 502 /** 503 * Interface used to format current value into a string for presentation. 504 */ 505 public interface Formatter { 506 507 /** 508 * Formats a string representation of the current value. 509 * 510 * @param value The currently selected value. 511 * @return A formatted string representation. 512 */ 513 public String format(int value); 514 } 515 516 /** 517 * Create a new number picker. 518 * 519 * @param context The application environment. 520 */ 521 public NumberPicker(Context context) { 522 this(context, null); 523 } 524 525 /** 526 * Create a new number picker. 527 * 528 * @param context The application environment. 529 * @param attrs A collection of attributes. 530 */ 531 public NumberPicker(Context context, AttributeSet attrs) { 532 this(context, attrs, R.attr.numberPickerStyle); 533 } 534 535 /** 536 * Create a new number picker 537 * 538 * @param context the application environment. 539 * @param attrs a collection of attributes. 540 * @param defStyle The default style to apply to this view. 541 */ 542 public NumberPicker(Context context, AttributeSet attrs, int defStyle) { 543 super(context, attrs, defStyle); 544 545 // process style attributes 546 TypedArray attributesArray = context.obtainStyledAttributes(attrs, 547 R.styleable.NumberPicker, defStyle, 0); 548 mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); 549 mFlingable = attributesArray.getBoolean(R.styleable.NumberPicker_flingable, true); 550 mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider); 551 int defSelectionDividerHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 552 UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, 553 getResources().getDisplayMetrics()); 554 mSelectionDividerHeight = attributesArray.getDimensionPixelSize( 555 R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight); 556 mMinHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_minHeight, 557 SIZE_UNSPECIFIED); 558 mMaxHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_maxHeight, 559 SIZE_UNSPECIFIED); 560 if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED 561 && mMinHeight > mMaxHeight) { 562 throw new IllegalArgumentException("minHeight > maxHeight"); 563 } 564 mMinWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_minWidth, 565 SIZE_UNSPECIFIED); 566 mMaxWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_maxWidth, 567 SIZE_UNSPECIFIED); 568 if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED 569 && mMinWidth > mMaxWidth) { 570 throw new IllegalArgumentException("minWidth > maxWidth"); 571 } 572 mComputeMaxWidth = (mMaxWidth == Integer.MAX_VALUE); 573 attributesArray.recycle(); 574 575 mShowInputControlsAnimimationDuration = getResources().getInteger( 576 R.integer.config_longAnimTime); 577 578 // By default Linearlayout that we extend is not drawn. This is 579 // its draw() method is not called but dispatchDraw() is called 580 // directly (see ViewGroup.drawChild()). However, this class uses 581 // the fading edge effect implemented by View and we need our 582 // draw() method to be called. Therefore, we declare we will draw. 583 setWillNotDraw(false); 584 setSelectorWheelState(SELECTOR_WHEEL_STATE_NONE); 585 586 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 587 Context.LAYOUT_INFLATER_SERVICE); 588 inflater.inflate(R.layout.number_picker, this, true); 589 590 OnClickListener onClickListener = new OnClickListener() { 591 public void onClick(View v) { 592 hideSoftInput(); 593 mInputText.clearFocus(); 594 if (v.getId() == R.id.increment) { 595 changeCurrentByOne(true); 596 } else { 597 changeCurrentByOne(false); 598 } 599 } 600 }; 601 602 OnLongClickListener onLongClickListener = new OnLongClickListener() { 603 public boolean onLongClick(View v) { 604 hideSoftInput(); 605 mInputText.clearFocus(); 606 if (v.getId() == R.id.increment) { 607 postChangeCurrentByOneFromLongPress(true); 608 } else { 609 postChangeCurrentByOneFromLongPress(false); 610 } 611 return true; 612 } 613 }; 614 615 // increment button 616 mIncrementButton = (ImageButton) findViewById(R.id.increment); 617 mIncrementButton.setOnClickListener(onClickListener); 618 mIncrementButton.setOnLongClickListener(onLongClickListener); 619 620 // decrement button 621 mDecrementButton = (ImageButton) findViewById(R.id.decrement); 622 mDecrementButton.setOnClickListener(onClickListener); 623 mDecrementButton.setOnLongClickListener(onLongClickListener); 624 625 // input text 626 mInputText = (EditText) findViewById(R.id.numberpicker_input); 627 mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { 628 public void onFocusChange(View v, boolean hasFocus) { 629 if (hasFocus) { 630 mInputText.selectAll(); 631 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 632 if (inputMethodManager != null) { 633 inputMethodManager.showSoftInput(mInputText, 0); 634 } 635 } else { 636 mInputText.setSelection(0, 0); 637 validateInputTextView(v); 638 } 639 } 640 }); 641 mInputText.setFilters(new InputFilter[] { 642 new InputTextFilter() 643 }); 644 645 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 646 647 // initialize constants 648 mTouchSlop = ViewConfiguration.getTapTimeout(); 649 ViewConfiguration configuration = ViewConfiguration.get(context); 650 mTouchSlop = configuration.getScaledTouchSlop(); 651 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 652 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() 653 / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; 654 mTextSize = (int) mInputText.getTextSize(); 655 656 // create the selector wheel paint 657 Paint paint = new Paint(); 658 paint.setAntiAlias(true); 659 paint.setTextAlign(Align.CENTER); 660 paint.setTextSize(mTextSize); 661 paint.setTypeface(mInputText.getTypeface()); 662 ColorStateList colors = mInputText.getTextColors(); 663 int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); 664 paint.setColor(color); 665 mSelectorWheelPaint = paint; 666 667 // create the animator for showing the input controls 668 mDimSelectorWheelAnimator = ObjectAnimator.ofInt(this, PROPERTY_SELECTOR_PAINT_ALPHA, 669 SELECTOR_WHEEL_BRIGHT_ALPHA, SELECTOR_WHEEL_DIM_ALPHA); 670 final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton, 671 PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE); 672 final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton, 673 PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE); 674 mShowInputControlsAnimator = new AnimatorSet(); 675 mShowInputControlsAnimator.playTogether(mDimSelectorWheelAnimator, showIncrementButton, 676 showDecrementButton); 677 mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() { 678 private boolean mCanceled = false; 679 680 @Override 681 public void onAnimationEnd(Animator animation) { 682 if (!mCanceled) { 683 // if canceled => we still want the wheel drawn 684 setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL); 685 } 686 mCanceled = false; 687 } 688 689 @Override 690 public void onAnimationCancel(Animator animation) { 691 if (mShowInputControlsAnimator.isRunning()) { 692 mCanceled = true; 693 } 694 } 695 }); 696 697 // create the fling and adjust scrollers 698 mFlingScroller = new Scroller(getContext(), null, true); 699 mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); 700 701 updateInputTextView(); 702 updateIncrementAndDecrementButtonsVisibilityState(); 703 704 if (mFlingable) { 705 if (isInEditMode()) { 706 setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL); 707 } else { 708 // Start with shown selector wheel and hidden controls. When made 709 // visible hide the selector and fade-in the controls to suggest 710 // fling interaction. 711 setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); 712 hideInputControls(); 713 } 714 } 715 } 716 717 @Override 718 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 719 final int msrdWdth = getMeasuredWidth(); 720 final int msrdHght = getMeasuredHeight(); 721 722 // Increment button at the top. 723 final int inctBtnMsrdWdth = mIncrementButton.getMeasuredWidth(); 724 final int incrBtnLeft = (msrdWdth - inctBtnMsrdWdth) / 2; 725 final int incrBtnTop = 0; 726 final int incrBtnRight = incrBtnLeft + inctBtnMsrdWdth; 727 final int incrBtnBottom = incrBtnTop + mIncrementButton.getMeasuredHeight(); 728 mIncrementButton.layout(incrBtnLeft, incrBtnTop, incrBtnRight, incrBtnBottom); 729 730 // Input text centered horizontally. 731 final int inptTxtMsrdWdth = mInputText.getMeasuredWidth(); 732 final int inptTxtMsrdHght = mInputText.getMeasuredHeight(); 733 final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2; 734 final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2; 735 final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth; 736 final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; 737 mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); 738 739 // Decrement button at the top. 740 final int decrBtnMsrdWdth = mIncrementButton.getMeasuredWidth(); 741 final int decrBtnLeft = (msrdWdth - decrBtnMsrdWdth) / 2; 742 final int decrBtnTop = msrdHght - mDecrementButton.getMeasuredHeight(); 743 final int decrBtnRight = decrBtnLeft + decrBtnMsrdWdth; 744 final int decrBtnBottom = msrdHght; 745 mDecrementButton.layout(decrBtnLeft, decrBtnTop, decrBtnRight, decrBtnBottom); 746 747 if (!mScrollWheelAndFadingEdgesInitialized) { 748 mScrollWheelAndFadingEdgesInitialized = true; 749 // need to do all this when we know our size 750 initializeSelectorWheel(); 751 initializeFadingEdges(); 752 } 753 } 754 755 @Override 756 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 757 // Try greedily to fit the max width and height. 758 final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); 759 final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); 760 super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); 761 // Flag if we are measured with width or height less than the respective min. 762 final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), 763 widthMeasureSpec); 764 final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), 765 heightMeasureSpec); 766 setMeasuredDimension(widthSize, heightSize); 767 } 768 769 @Override 770 public boolean onInterceptTouchEvent(MotionEvent event) { 771 if (!isEnabled() || !mFlingable) { 772 return false; 773 } 774 switch (event.getActionMasked()) { 775 case MotionEvent.ACTION_DOWN: 776 mLastMotionEventY = mLastDownEventY = event.getY(); 777 removeAllCallbacks(); 778 mShowInputControlsAnimator.cancel(); 779 mDimSelectorWheelAnimator.cancel(); 780 mBeginEditOnUpEvent = false; 781 mAdjustScrollerOnUpEvent = true; 782 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { 783 mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); 784 boolean scrollersFinished = mFlingScroller.isFinished() 785 && mAdjustScroller.isFinished(); 786 if (!scrollersFinished) { 787 mFlingScroller.forceFinished(true); 788 mAdjustScroller.forceFinished(true); 789 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 790 } 791 mBeginEditOnUpEvent = scrollersFinished; 792 mAdjustScrollerOnUpEvent = true; 793 hideSoftInput(); 794 hideInputControls(); 795 return true; 796 } 797 if (isEventInVisibleViewHitRect(event, mIncrementButton) 798 || isEventInVisibleViewHitRect(event, mDecrementButton)) { 799 return false; 800 } 801 mAdjustScrollerOnUpEvent = false; 802 setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); 803 hideSoftInput(); 804 hideInputControls(); 805 return true; 806 case MotionEvent.ACTION_MOVE: 807 float currentMoveY = event.getY(); 808 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); 809 if (deltaDownY > mTouchSlop) { 810 mBeginEditOnUpEvent = false; 811 onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 812 setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); 813 hideSoftInput(); 814 hideInputControls(); 815 return true; 816 } 817 break; 818 } 819 return false; 820 } 821 822 @Override 823 public boolean onTouchEvent(MotionEvent ev) { 824 if (!isEnabled()) { 825 return false; 826 } 827 if (mVelocityTracker == null) { 828 mVelocityTracker = VelocityTracker.obtain(); 829 } 830 mVelocityTracker.addMovement(ev); 831 int action = ev.getActionMasked(); 832 switch (action) { 833 case MotionEvent.ACTION_MOVE: 834 float currentMoveY = ev.getY(); 835 if (mBeginEditOnUpEvent 836 || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 837 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); 838 if (deltaDownY > mTouchSlop) { 839 mBeginEditOnUpEvent = false; 840 onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 841 } 842 } 843 int deltaMoveY = (int) (currentMoveY - mLastMotionEventY); 844 scrollBy(0, deltaMoveY); 845 invalidate(); 846 mLastMotionEventY = currentMoveY; 847 break; 848 case MotionEvent.ACTION_UP: 849 if (mBeginEditOnUpEvent) { 850 setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL); 851 showInputControls(mShowInputControlsAnimimationDuration); 852 mInputText.requestFocus(); 853 return true; 854 } 855 VelocityTracker velocityTracker = mVelocityTracker; 856 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 857 int initialVelocity = (int) velocityTracker.getYVelocity(); 858 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { 859 fling(initialVelocity); 860 onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 861 } else { 862 if (mAdjustScrollerOnUpEvent) { 863 if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) { 864 postAdjustScrollerCommand(0); 865 } 866 } else { 867 postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS); 868 } 869 } 870 mVelocityTracker.recycle(); 871 mVelocityTracker = null; 872 break; 873 } 874 return true; 875 } 876 877 @Override 878 public boolean dispatchTouchEvent(MotionEvent event) { 879 final int action = event.getActionMasked(); 880 switch (action) { 881 case MotionEvent.ACTION_MOVE: 882 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { 883 removeAllCallbacks(); 884 forceCompleteChangeCurrentByOneViaScroll(); 885 } 886 break; 887 case MotionEvent.ACTION_CANCEL: 888 case MotionEvent.ACTION_UP: 889 removeAllCallbacks(); 890 break; 891 } 892 return super.dispatchTouchEvent(event); 893 } 894 895 @Override 896 public boolean dispatchKeyEvent(KeyEvent event) { 897 int keyCode = event.getKeyCode(); 898 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { 899 removeAllCallbacks(); 900 } 901 return super.dispatchKeyEvent(event); 902 } 903 904 @Override 905 public boolean dispatchTrackballEvent(MotionEvent event) { 906 int action = event.getActionMasked(); 907 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 908 removeAllCallbacks(); 909 } 910 return super.dispatchTrackballEvent(event); 911 } 912 913 @Override 914 public void computeScroll() { 915 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { 916 return; 917 } 918 Scroller scroller = mFlingScroller; 919 if (scroller.isFinished()) { 920 scroller = mAdjustScroller; 921 if (scroller.isFinished()) { 922 return; 923 } 924 } 925 scroller.computeScrollOffset(); 926 int currentScrollerY = scroller.getCurrY(); 927 if (mPreviousScrollerY == 0) { 928 mPreviousScrollerY = scroller.getStartY(); 929 } 930 scrollBy(0, currentScrollerY - mPreviousScrollerY); 931 mPreviousScrollerY = currentScrollerY; 932 if (scroller.isFinished()) { 933 onScrollerFinished(scroller); 934 } else { 935 invalidate(); 936 } 937 } 938 939 @Override 940 public void setEnabled(boolean enabled) { 941 super.setEnabled(enabled); 942 mIncrementButton.setEnabled(enabled); 943 mDecrementButton.setEnabled(enabled); 944 mInputText.setEnabled(enabled); 945 } 946 947 @Override 948 public void scrollBy(int x, int y) { 949 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { 950 return; 951 } 952 int[] selectorIndices = mSelectorIndices; 953 if (!mWrapSelectorWheel && y > 0 954 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 955 mCurrentScrollOffset = mInitialScrollOffset; 956 return; 957 } 958 if (!mWrapSelectorWheel && y < 0 959 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 960 mCurrentScrollOffset = mInitialScrollOffset; 961 return; 962 } 963 mCurrentScrollOffset += y; 964 while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { 965 mCurrentScrollOffset -= mSelectorElementHeight; 966 decrementSelectorIndices(selectorIndices); 967 changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); 968 if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 969 mCurrentScrollOffset = mInitialScrollOffset; 970 } 971 } 972 while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { 973 mCurrentScrollOffset += mSelectorElementHeight; 974 incrementSelectorIndices(selectorIndices); 975 changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); 976 if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 977 mCurrentScrollOffset = mInitialScrollOffset; 978 } 979 } 980 } 981 982 @Override 983 public int getSolidColor() { 984 return mSolidColor; 985 } 986 987 /** 988 * Sets the listener to be notified on change of the current value. 989 * 990 * @param onValueChangedListener The listener. 991 */ 992 public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { 993 mOnValueChangeListener = onValueChangedListener; 994 } 995 996 /** 997 * Set listener to be notified for scroll state changes. 998 * 999 * @param onScrollListener The listener. 1000 */ 1001 public void setOnScrollListener(OnScrollListener onScrollListener) { 1002 mOnScrollListener = onScrollListener; 1003 } 1004 1005 /** 1006 * Set the formatter to be used for formatting the current value. 1007 * <p> 1008 * Note: If you have provided alternative values for the values this 1009 * formatter is never invoked. 1010 * </p> 1011 * 1012 * @param formatter The formatter object. If formatter is <code>null</code>, 1013 * {@link String#valueOf(int)} will be used. 1014 * 1015 * @see #setDisplayedValues(String[]) 1016 */ 1017 public void setFormatter(Formatter formatter) { 1018 if (formatter == mFormatter) { 1019 return; 1020 } 1021 mFormatter = formatter; 1022 initializeSelectorWheelIndices(); 1023 updateInputTextView(); 1024 } 1025 1026 /** 1027 * Set the current value for the number picker. 1028 * <p> 1029 * If the argument is less than the {@link NumberPicker#getMinValue()} and 1030 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 1031 * current value is set to the {@link NumberPicker#getMinValue()} value. 1032 * </p> 1033 * <p> 1034 * If the argument is less than the {@link NumberPicker#getMinValue()} and 1035 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 1036 * current value is set to the {@link NumberPicker#getMaxValue()} value. 1037 * </p> 1038 * <p> 1039 * If the argument is less than the {@link NumberPicker#getMaxValue()} and 1040 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 1041 * current value is set to the {@link NumberPicker#getMaxValue()} value. 1042 * </p> 1043 * <p> 1044 * If the argument is less than the {@link NumberPicker#getMaxValue()} and 1045 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 1046 * current value is set to the {@link NumberPicker#getMinValue()} value. 1047 * </p> 1048 * 1049 * @param value The current value. 1050 * @see #setWrapSelectorWheel(boolean) 1051 * @see #setMinValue(int) 1052 * @see #setMaxValue(int) 1053 */ 1054 public void setValue(int value) { 1055 if (mValue == value) { 1056 return; 1057 } 1058 if (value < mMinValue) { 1059 value = mWrapSelectorWheel ? mMaxValue : mMinValue; 1060 } 1061 if (value > mMaxValue) { 1062 value = mWrapSelectorWheel ? mMinValue : mMaxValue; 1063 } 1064 mValue = value; 1065 initializeSelectorWheelIndices(); 1066 updateInputTextView(); 1067 updateIncrementAndDecrementButtonsVisibilityState(); 1068 invalidate(); 1069 } 1070 1071 /** 1072 * Hides the soft input of it is active for the input text. 1073 */ 1074 private void hideSoftInput() { 1075 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 1076 if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { 1077 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 1078 } 1079 } 1080 1081 /** 1082 * Computes the max width if no such specified as an attribute. 1083 */ 1084 private void tryComputeMaxWidth() { 1085 if (!mComputeMaxWidth) { 1086 return; 1087 } 1088 int maxTextWidth = 0; 1089 if (mDisplayedValues == null) { 1090 float maxDigitWidth = 0; 1091 for (int i = 0; i <= 9; i++) { 1092 final float digitWidth = mSelectorWheelPaint.measureText(String.valueOf(i)); 1093 if (digitWidth > maxDigitWidth) { 1094 maxDigitWidth = digitWidth; 1095 } 1096 } 1097 int numberOfDigits = 0; 1098 int current = mMaxValue; 1099 while (current > 0) { 1100 numberOfDigits++; 1101 current = current / 10; 1102 } 1103 maxTextWidth = (int) (numberOfDigits * maxDigitWidth); 1104 } else { 1105 final int valueCount = mDisplayedValues.length; 1106 for (int i = 0; i < valueCount; i++) { 1107 final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]); 1108 if (textWidth > maxTextWidth) { 1109 maxTextWidth = (int) textWidth; 1110 } 1111 } 1112 } 1113 maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight(); 1114 if (mMaxWidth != maxTextWidth) { 1115 if (maxTextWidth > mMinWidth) { 1116 mMaxWidth = maxTextWidth; 1117 } else { 1118 mMaxWidth = mMinWidth; 1119 } 1120 invalidate(); 1121 } 1122 } 1123 1124 /** 1125 * Gets whether the selector wheel wraps when reaching the min/max value. 1126 * 1127 * @return True if the selector wheel wraps. 1128 * 1129 * @see #getMinValue() 1130 * @see #getMaxValue() 1131 */ 1132 public boolean getWrapSelectorWheel() { 1133 return mWrapSelectorWheel; 1134 } 1135 1136 /** 1137 * Sets whether the selector wheel shown during flinging/scrolling should 1138 * wrap around the {@link NumberPicker#getMinValue()} and 1139 * {@link NumberPicker#getMaxValue()} values. 1140 * <p> 1141 * By default if the range (max - min) is more than five (the number of 1142 * items shown on the selector wheel) the selector wheel wrapping is 1143 * enabled. 1144 * </p> 1145 * <p> 1146 * <strong>Note:</strong> If the number of items, i.e. the range 1147 * ({@link #getMaxValue()} - {@link #getMinValue()}) is less than 1148 * {@link #SELECTOR_WHEEL_ITEM_COUNT}, the selector wheel will not 1149 * wrap. Hence, in such a case calling this method is a NOP. 1150 * </p> 1151 * @param wrapSelectorWheel Whether to wrap. 1152 */ 1153 public void setWrapSelectorWheel(boolean wrapSelectorWheel) { 1154 final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; 1155 if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) { 1156 mWrapSelectorWheel = wrapSelectorWheel; 1157 updateIncrementAndDecrementButtonsVisibilityState(); 1158 } 1159 } 1160 1161 /** 1162 * Sets the speed at which the numbers be incremented and decremented when 1163 * the up and down buttons are long pressed respectively. 1164 * <p> 1165 * The default value is 300 ms. 1166 * </p> 1167 * 1168 * @param intervalMillis The speed (in milliseconds) at which the numbers 1169 * will be incremented and decremented. 1170 */ 1171 public void setOnLongPressUpdateInterval(long intervalMillis) { 1172 mLongPressUpdateInterval = intervalMillis; 1173 } 1174 1175 /** 1176 * Returns the value of the picker. 1177 * 1178 * @return The value. 1179 */ 1180 public int getValue() { 1181 return mValue; 1182 } 1183 1184 /** 1185 * Returns the min value of the picker. 1186 * 1187 * @return The min value 1188 */ 1189 public int getMinValue() { 1190 return mMinValue; 1191 } 1192 1193 /** 1194 * Sets the min value of the picker. 1195 * 1196 * @param minValue The min value. 1197 */ 1198 public void setMinValue(int minValue) { 1199 if (mMinValue == minValue) { 1200 return; 1201 } 1202 if (minValue < 0) { 1203 throw new IllegalArgumentException("minValue must be >= 0"); 1204 } 1205 mMinValue = minValue; 1206 if (mMinValue > mValue) { 1207 mValue = mMinValue; 1208 } 1209 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; 1210 setWrapSelectorWheel(wrapSelectorWheel); 1211 initializeSelectorWheelIndices(); 1212 updateInputTextView(); 1213 tryComputeMaxWidth(); 1214 } 1215 1216 /** 1217 * Returns the max value of the picker. 1218 * 1219 * @return The max value. 1220 */ 1221 public int getMaxValue() { 1222 return mMaxValue; 1223 } 1224 1225 /** 1226 * Sets the max value of the picker. 1227 * 1228 * @param maxValue The max value. 1229 */ 1230 public void setMaxValue(int maxValue) { 1231 if (mMaxValue == maxValue) { 1232 return; 1233 } 1234 if (maxValue < 0) { 1235 throw new IllegalArgumentException("maxValue must be >= 0"); 1236 } 1237 mMaxValue = maxValue; 1238 if (mMaxValue < mValue) { 1239 mValue = mMaxValue; 1240 } 1241 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; 1242 setWrapSelectorWheel(wrapSelectorWheel); 1243 initializeSelectorWheelIndices(); 1244 updateInputTextView(); 1245 tryComputeMaxWidth(); 1246 } 1247 1248 /** 1249 * Gets the values to be displayed instead of string values. 1250 * 1251 * @return The displayed values. 1252 */ 1253 public String[] getDisplayedValues() { 1254 return mDisplayedValues; 1255 } 1256 1257 /** 1258 * Sets the values to be displayed. 1259 * 1260 * @param displayedValues The displayed values. 1261 */ 1262 public void setDisplayedValues(String[] displayedValues) { 1263 if (mDisplayedValues == displayedValues) { 1264 return; 1265 } 1266 mDisplayedValues = displayedValues; 1267 if (mDisplayedValues != null) { 1268 // Allow text entry rather than strictly numeric entry. 1269 mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT 1270 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 1271 } else { 1272 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 1273 } 1274 updateInputTextView(); 1275 initializeSelectorWheelIndices(); 1276 tryComputeMaxWidth(); 1277 } 1278 1279 @Override 1280 protected float getTopFadingEdgeStrength() { 1281 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1282 } 1283 1284 @Override 1285 protected float getBottomFadingEdgeStrength() { 1286 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1287 } 1288 1289 @Override 1290 protected void onAttachedToWindow() { 1291 super.onAttachedToWindow(); 1292 // make sure we show the controls only the very 1293 // first time the user sees this widget 1294 if (mFlingable && !isInEditMode()) { 1295 // animate a bit slower the very first time 1296 showInputControls(mShowInputControlsAnimimationDuration * 2); 1297 } 1298 } 1299 1300 @Override 1301 protected void onDetachedFromWindow() { 1302 removeAllCallbacks(); 1303 } 1304 1305 @Override 1306 protected void dispatchDraw(Canvas canvas) { 1307 // There is a good reason for doing this. See comments in draw(). 1308 } 1309 1310 @Override 1311 public void draw(Canvas canvas) { 1312 // Dispatch draw to our children only if we are not currently running 1313 // the animation for simultaneously dimming the scroll wheel and 1314 // showing in the buttons. This class takes advantage of the View 1315 // implementation of fading edges effect to draw the selector wheel. 1316 // However, in View.draw(), the fading is applied after all the children 1317 // have been drawn and we do not want this fading to be applied to the 1318 // buttons. Therefore, we draw our children after we have completed 1319 // drawing ourselves. 1320 super.draw(canvas); 1321 1322 // Draw our children if we are not showing the selector wheel of fading 1323 // it out 1324 if (mShowInputControlsAnimator.isRunning() 1325 || mSelectorWheelState != SELECTOR_WHEEL_STATE_LARGE) { 1326 long drawTime = getDrawingTime(); 1327 for (int i = 0, count = getChildCount(); i < count; i++) { 1328 View child = getChildAt(i); 1329 if (!child.isShown()) { 1330 continue; 1331 } 1332 drawChild(canvas, getChildAt(i), drawTime); 1333 } 1334 } 1335 } 1336 1337 @Override 1338 protected void onDraw(Canvas canvas) { 1339 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { 1340 return; 1341 } 1342 1343 float x = (mRight - mLeft) / 2; 1344 float y = mCurrentScrollOffset; 1345 1346 final int restoreCount = canvas.save(); 1347 1348 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_SMALL) { 1349 Rect clipBounds = canvas.getClipBounds(); 1350 clipBounds.inset(0, mSelectorElementHeight); 1351 canvas.clipRect(clipBounds); 1352 } 1353 1354 // draw the selector wheel 1355 int[] selectorIndices = mSelectorIndices; 1356 for (int i = 0; i < selectorIndices.length; i++) { 1357 int selectorIndex = selectorIndices[i]; 1358 String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); 1359 // Do not draw the middle item if input is visible since the input is shown only 1360 // if the wheel is static and it covers the middle item. Otherwise, if the user 1361 // starts editing the text via the IME he may see a dimmed version of the old 1362 // value intermixed with the new one. 1363 if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) { 1364 canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); 1365 } 1366 y += mSelectorElementHeight; 1367 } 1368 1369 // draw the selection dividers (only if scrolling and drawable specified) 1370 if (mSelectionDivider != null) { 1371 // draw the top divider 1372 int topOfTopDivider = 1373 (getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2; 1374 int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; 1375 mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); 1376 mSelectionDivider.draw(canvas); 1377 1378 // draw the bottom divider 1379 int topOfBottomDivider = topOfTopDivider + mSelectorElementHeight; 1380 int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight; 1381 mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); 1382 mSelectionDivider.draw(canvas); 1383 } 1384 1385 canvas.restoreToCount(restoreCount); 1386 } 1387 1388 @Override 1389 public void sendAccessibilityEvent(int eventType) { 1390 // Do not send accessibility events - we want the user to 1391 // perceive this widget as several controls rather as a whole. 1392 } 1393 1394 @Override 1395 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1396 super.onInitializeAccessibilityEvent(event); 1397 event.setClassName(NumberPicker.class.getName()); 1398 } 1399 1400 @Override 1401 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1402 super.onInitializeAccessibilityNodeInfo(info); 1403 info.setClassName(NumberPicker.class.getName()); 1404 } 1405 1406 /** 1407 * Makes a measure spec that tries greedily to use the max value. 1408 * 1409 * @param measureSpec The measure spec. 1410 * @param maxSize The max value for the size. 1411 * @return A measure spec greedily imposing the max size. 1412 */ 1413 private int makeMeasureSpec(int measureSpec, int maxSize) { 1414 if (maxSize == SIZE_UNSPECIFIED) { 1415 return measureSpec; 1416 } 1417 final int size = MeasureSpec.getSize(measureSpec); 1418 final int mode = MeasureSpec.getMode(measureSpec); 1419 switch (mode) { 1420 case MeasureSpec.EXACTLY: 1421 return measureSpec; 1422 case MeasureSpec.AT_MOST: 1423 return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); 1424 case MeasureSpec.UNSPECIFIED: 1425 return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); 1426 default: 1427 throw new IllegalArgumentException("Unknown measure mode: " + mode); 1428 } 1429 } 1430 1431 /** 1432 * Utility to reconcile a desired size and state, with constraints imposed by 1433 * a MeasureSpec. Tries to respect the min size, unless a different size is 1434 * imposed by the constraints. 1435 * 1436 * @param minSize The minimal desired size. 1437 * @param measuredSize The currently measured size. 1438 * @param measureSpec The current measure spec. 1439 * @return The resolved size and state. 1440 */ 1441 private int resolveSizeAndStateRespectingMinSize(int minSize, int measuredSize, 1442 int measureSpec) { 1443 if (minSize != SIZE_UNSPECIFIED) { 1444 final int desiredWidth = Math.max(minSize, measuredSize); 1445 return resolveSizeAndState(desiredWidth, measureSpec, 0); 1446 } else { 1447 return measuredSize; 1448 } 1449 } 1450 1451 /** 1452 * Resets the selector indices and clear the cached 1453 * string representation of these indices. 1454 */ 1455 private void initializeSelectorWheelIndices() { 1456 mSelectorIndexToStringCache.clear(); 1457 int[] selectorIdices = mSelectorIndices; 1458 int current = getValue(); 1459 for (int i = 0; i < mSelectorIndices.length; i++) { 1460 int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); 1461 if (mWrapSelectorWheel) { 1462 selectorIndex = getWrappedSelectorIndex(selectorIndex); 1463 } 1464 mSelectorIndices[i] = selectorIndex; 1465 ensureCachedScrollSelectorValue(mSelectorIndices[i]); 1466 } 1467 } 1468 1469 /** 1470 * Sets the current value of this NumberPicker, and sets mPrevious to the 1471 * previous value. If current is greater than mEnd less than mStart, the 1472 * value of mCurrent is wrapped around. Subclasses can override this to 1473 * change the wrapping behavior 1474 * 1475 * @param current the new value of the NumberPicker 1476 */ 1477 private void changeCurrent(int current) { 1478 if (mValue == current) { 1479 return; 1480 } 1481 // Wrap around the values if we go past the start or end 1482 if (mWrapSelectorWheel) { 1483 current = getWrappedSelectorIndex(current); 1484 } 1485 int previous = mValue; 1486 setValue(current); 1487 notifyChange(previous, current); 1488 } 1489 1490 /** 1491 * Changes the current value by one which is increment or 1492 * decrement based on the passes argument. 1493 * 1494 * @param increment True to increment, false to decrement. 1495 */ 1496 private void changeCurrentByOne(boolean increment) { 1497 if (mFlingable) { 1498 mDimSelectorWheelAnimator.cancel(); 1499 mInputText.setVisibility(View.INVISIBLE); 1500 mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); 1501 mPreviousScrollerY = 0; 1502 forceCompleteChangeCurrentByOneViaScroll(); 1503 if (increment) { 1504 mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, 1505 CHANGE_CURRENT_BY_ONE_SCROLL_DURATION); 1506 } else { 1507 mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, 1508 CHANGE_CURRENT_BY_ONE_SCROLL_DURATION); 1509 } 1510 invalidate(); 1511 } else { 1512 if (increment) { 1513 changeCurrent(mValue + 1); 1514 } else { 1515 changeCurrent(mValue - 1); 1516 } 1517 } 1518 } 1519 1520 /** 1521 * Ensures that if we are in the process of changing the current value 1522 * by one via scrolling the scroller gets to its final state and the 1523 * value is updated. 1524 */ 1525 private void forceCompleteChangeCurrentByOneViaScroll() { 1526 Scroller scroller = mFlingScroller; 1527 if (!scroller.isFinished()) { 1528 final int yBeforeAbort = scroller.getCurrY(); 1529 scroller.abortAnimation(); 1530 final int yDelta = scroller.getCurrY() - yBeforeAbort; 1531 scrollBy(0, yDelta); 1532 } 1533 } 1534 1535 /** 1536 * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector 1537 * wheel. 1538 */ 1539 @SuppressWarnings("unused") 1540 // Called via reflection 1541 private void setSelectorPaintAlpha(int alpha) { 1542 mSelectorWheelPaint.setAlpha(alpha); 1543 invalidate(); 1544 } 1545 1546 /** 1547 * @return If the <code>event</code> is in the visible <code>view</code>. 1548 */ 1549 private boolean isEventInVisibleViewHitRect(MotionEvent event, View view) { 1550 if (view.getVisibility() == VISIBLE) { 1551 view.getHitRect(mTempRect); 1552 return mTempRect.contains((int) event.getX(), (int) event.getY()); 1553 } 1554 return false; 1555 } 1556 1557 /** 1558 * Sets the <code>selectorWheelState</code>. 1559 */ 1560 private void setSelectorWheelState(int selectorWheelState) { 1561 mSelectorWheelState = selectorWheelState; 1562 if (selectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { 1563 mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); 1564 } 1565 1566 if (mFlingable && selectorWheelState == SELECTOR_WHEEL_STATE_LARGE 1567 && AccessibilityManager.getInstance(mContext).isEnabled()) { 1568 AccessibilityManager.getInstance(mContext).interrupt(); 1569 String text = mContext.getString(R.string.number_picker_increment_scroll_action); 1570 mInputText.setContentDescription(text); 1571 mInputText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1572 mInputText.setContentDescription(null); 1573 } 1574 } 1575 1576 private void initializeSelectorWheel() { 1577 initializeSelectorWheelIndices(); 1578 int[] selectorIndices = mSelectorIndices; 1579 int totalTextHeight = selectorIndices.length * mTextSize; 1580 float totalTextGapHeight = (mBottom - mTop) - totalTextHeight; 1581 float textGapCount = selectorIndices.length - 1; 1582 mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f); 1583 mSelectorElementHeight = mTextSize + mSelectorTextGapHeight; 1584 // Ensure that the middle item is positioned the same as the text in mInputText 1585 int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop(); 1586 mInitialScrollOffset = editTextTextPosition - 1587 (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); 1588 mCurrentScrollOffset = mInitialScrollOffset; 1589 updateInputTextView(); 1590 } 1591 1592 private void initializeFadingEdges() { 1593 setVerticalFadingEdgeEnabled(true); 1594 setFadingEdgeLength((mBottom - mTop - mTextSize) / 2); 1595 } 1596 1597 /** 1598 * Callback invoked upon completion of a given <code>scroller</code>. 1599 */ 1600 private void onScrollerFinished(Scroller scroller) { 1601 if (scroller == mFlingScroller) { 1602 if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { 1603 postAdjustScrollerCommand(0); 1604 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 1605 } else { 1606 updateInputTextView(); 1607 fadeSelectorWheel(mShowInputControlsAnimimationDuration); 1608 } 1609 } else { 1610 updateInputTextView(); 1611 showInputControls(mShowInputControlsAnimimationDuration); 1612 } 1613 } 1614 1615 /** 1616 * Handles transition to a given <code>scrollState</code> 1617 */ 1618 private void onScrollStateChange(int scrollState) { 1619 if (mScrollState == scrollState) { 1620 return; 1621 } 1622 mScrollState = scrollState; 1623 if (mOnScrollListener != null) { 1624 mOnScrollListener.onScrollStateChange(this, scrollState); 1625 } 1626 } 1627 1628 /** 1629 * Flings the selector with the given <code>velocityY</code>. 1630 */ 1631 private void fling(int velocityY) { 1632 mPreviousScrollerY = 0; 1633 1634 if (velocityY > 0) { 1635 mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1636 } else { 1637 mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1638 } 1639 1640 invalidate(); 1641 } 1642 1643 /** 1644 * Hides the input controls which is the up/down arrows and the text field. 1645 */ 1646 private void hideInputControls() { 1647 mShowInputControlsAnimator.cancel(); 1648 mIncrementButton.setVisibility(INVISIBLE); 1649 mDecrementButton.setVisibility(INVISIBLE); 1650 mInputText.setVisibility(INVISIBLE); 1651 } 1652 1653 /** 1654 * Show the input controls by making them visible and animating the alpha 1655 * property up/down arrows. 1656 * 1657 * @param animationDuration The duration of the animation. 1658 */ 1659 private void showInputControls(long animationDuration) { 1660 updateIncrementAndDecrementButtonsVisibilityState(); 1661 mInputText.setVisibility(VISIBLE); 1662 mShowInputControlsAnimator.setDuration(animationDuration); 1663 mShowInputControlsAnimator.start(); 1664 } 1665 1666 /** 1667 * Fade the selector wheel via an animation. 1668 * 1669 * @param animationDuration The duration of the animation. 1670 */ 1671 private void fadeSelectorWheel(long animationDuration) { 1672 mInputText.setVisibility(VISIBLE); 1673 mDimSelectorWheelAnimator.setDuration(animationDuration); 1674 mDimSelectorWheelAnimator.start(); 1675 } 1676 1677 /** 1678 * Updates the visibility state of the increment and decrement buttons. 1679 */ 1680 private void updateIncrementAndDecrementButtonsVisibilityState() { 1681 if (mWrapSelectorWheel || mValue < mMaxValue) { 1682 mIncrementButton.setVisibility(VISIBLE); 1683 } else { 1684 mIncrementButton.setVisibility(INVISIBLE); 1685 } 1686 if (mWrapSelectorWheel || mValue > mMinValue) { 1687 mDecrementButton.setVisibility(VISIBLE); 1688 } else { 1689 mDecrementButton.setVisibility(INVISIBLE); 1690 } 1691 } 1692 1693 /** 1694 * @return The wrapped index <code>selectorIndex</code> value. 1695 */ 1696 private int getWrappedSelectorIndex(int selectorIndex) { 1697 if (selectorIndex > mMaxValue) { 1698 return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1; 1699 } else if (selectorIndex < mMinValue) { 1700 return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1; 1701 } 1702 return selectorIndex; 1703 } 1704 1705 /** 1706 * Increments the <code>selectorIndices</code> whose string representations 1707 * will be displayed in the selector. 1708 */ 1709 private void incrementSelectorIndices(int[] selectorIndices) { 1710 for (int i = 0; i < selectorIndices.length - 1; i++) { 1711 selectorIndices[i] = selectorIndices[i + 1]; 1712 } 1713 int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; 1714 if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) { 1715 nextScrollSelectorIndex = mMinValue; 1716 } 1717 selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; 1718 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1719 } 1720 1721 /** 1722 * Decrements the <code>selectorIndices</code> whose string representations 1723 * will be displayed in the selector. 1724 */ 1725 private void decrementSelectorIndices(int[] selectorIndices) { 1726 for (int i = selectorIndices.length - 1; i > 0; i--) { 1727 selectorIndices[i] = selectorIndices[i - 1]; 1728 } 1729 int nextScrollSelectorIndex = selectorIndices[1] - 1; 1730 if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) { 1731 nextScrollSelectorIndex = mMaxValue; 1732 } 1733 selectorIndices[0] = nextScrollSelectorIndex; 1734 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1735 } 1736 1737 /** 1738 * Ensures we have a cached string representation of the given <code> 1739 * selectorIndex</code> 1740 * to avoid multiple instantiations of the same string. 1741 */ 1742 private void ensureCachedScrollSelectorValue(int selectorIndex) { 1743 SparseArray<String> cache = mSelectorIndexToStringCache; 1744 String scrollSelectorValue = cache.get(selectorIndex); 1745 if (scrollSelectorValue != null) { 1746 return; 1747 } 1748 if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { 1749 scrollSelectorValue = ""; 1750 } else { 1751 if (mDisplayedValues != null) { 1752 int displayedValueIndex = selectorIndex - mMinValue; 1753 scrollSelectorValue = mDisplayedValues[displayedValueIndex]; 1754 } else { 1755 scrollSelectorValue = formatNumber(selectorIndex); 1756 } 1757 } 1758 cache.put(selectorIndex, scrollSelectorValue); 1759 } 1760 1761 private String formatNumber(int value) { 1762 return (mFormatter != null) ? mFormatter.format(value) : String.valueOf(value); 1763 } 1764 1765 private void validateInputTextView(View v) { 1766 String str = String.valueOf(((TextView) v).getText()); 1767 if (TextUtils.isEmpty(str)) { 1768 // Restore to the old value as we don't allow empty values 1769 updateInputTextView(); 1770 } else { 1771 // Check the new value and ensure it's in range 1772 int current = getSelectedPos(str.toString()); 1773 changeCurrent(current); 1774 } 1775 } 1776 1777 /** 1778 * Updates the view of this NumberPicker. If displayValues were specified in 1779 * the string corresponding to the index specified by the current value will 1780 * be returned. Otherwise, the formatter specified in {@link #setFormatter} 1781 * will be used to format the number. 1782 */ 1783 private void updateInputTextView() { 1784 /* 1785 * If we don't have displayed values then use the current number else 1786 * find the correct value in the displayed values for the current 1787 * number. 1788 */ 1789 if (mDisplayedValues == null) { 1790 mInputText.setText(formatNumber(mValue)); 1791 } else { 1792 mInputText.setText(mDisplayedValues[mValue - mMinValue]); 1793 } 1794 mInputText.setSelection(mInputText.getText().length()); 1795 1796 if (mFlingable && AccessibilityManager.getInstance(mContext).isEnabled()) { 1797 String text = mContext.getString(R.string.number_picker_increment_scroll_mode, 1798 mInputText.getText()); 1799 mInputText.setContentDescription(text); 1800 } 1801 } 1802 1803 /** 1804 * Notifies the listener, if registered, of a change of the value of this 1805 * NumberPicker. 1806 */ 1807 private void notifyChange(int previous, int current) { 1808 if (mOnValueChangeListener != null) { 1809 mOnValueChangeListener.onValueChange(this, previous, mValue); 1810 } 1811 } 1812 1813 /** 1814 * Posts a command for changing the current value by one. 1815 * 1816 * @param increment Whether to increment or decrement the value. 1817 */ 1818 private void postChangeCurrentByOneFromLongPress(boolean increment) { 1819 mInputText.clearFocus(); 1820 removeAllCallbacks(); 1821 if (mChangeCurrentByOneFromLongPressCommand == null) { 1822 mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); 1823 } 1824 mChangeCurrentByOneFromLongPressCommand.setIncrement(increment); 1825 post(mChangeCurrentByOneFromLongPressCommand); 1826 } 1827 1828 /** 1829 * Removes all pending callback from the message queue. 1830 */ 1831 private void removeAllCallbacks() { 1832 if (mChangeCurrentByOneFromLongPressCommand != null) { 1833 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 1834 } 1835 if (mAdjustScrollerCommand != null) { 1836 removeCallbacks(mAdjustScrollerCommand); 1837 } 1838 if (mSetSelectionCommand != null) { 1839 removeCallbacks(mSetSelectionCommand); 1840 } 1841 } 1842 1843 /** 1844 * @return The selected index given its displayed <code>value</code>. 1845 */ 1846 private int getSelectedPos(String value) { 1847 if (mDisplayedValues == null) { 1848 try { 1849 return Integer.parseInt(value); 1850 } catch (NumberFormatException e) { 1851 // Ignore as if it's not a number we don't care 1852 } 1853 } else { 1854 for (int i = 0; i < mDisplayedValues.length; i++) { 1855 // Don't force the user to type in jan when ja will do 1856 value = value.toLowerCase(); 1857 if (mDisplayedValues[i].toLowerCase().startsWith(value)) { 1858 return mMinValue + i; 1859 } 1860 } 1861 1862 /* 1863 * The user might have typed in a number into the month field i.e. 1864 * 10 instead of OCT so support that too. 1865 */ 1866 try { 1867 return Integer.parseInt(value); 1868 } catch (NumberFormatException e) { 1869 1870 // Ignore as if it's not a number we don't care 1871 } 1872 } 1873 return mMinValue; 1874 } 1875 1876 /** 1877 * Posts an {@link SetSelectionCommand} from the given <code>selectionStart 1878 * </code> to 1879 * <code>selectionEnd</code>. 1880 */ 1881 private void postSetSelectionCommand(int selectionStart, int selectionEnd) { 1882 if (mSetSelectionCommand == null) { 1883 mSetSelectionCommand = new SetSelectionCommand(); 1884 } else { 1885 removeCallbacks(mSetSelectionCommand); 1886 } 1887 mSetSelectionCommand.mSelectionStart = selectionStart; 1888 mSetSelectionCommand.mSelectionEnd = selectionEnd; 1889 post(mSetSelectionCommand); 1890 } 1891 1892 /** 1893 * Posts an {@link AdjustScrollerCommand} within the given <code> 1894 * delayMillis</code> 1895 * . 1896 */ 1897 private void postAdjustScrollerCommand(int delayMillis) { 1898 if (mAdjustScrollerCommand == null) { 1899 mAdjustScrollerCommand = new AdjustScrollerCommand(); 1900 } else { 1901 removeCallbacks(mAdjustScrollerCommand); 1902 } 1903 postDelayed(mAdjustScrollerCommand, delayMillis); 1904 } 1905 1906 /** 1907 * Filter for accepting only valid indices or prefixes of the string 1908 * representation of valid indices. 1909 */ 1910 class InputTextFilter extends NumberKeyListener { 1911 1912 // XXX This doesn't allow for range limits when controlled by a 1913 // soft input method! 1914 public int getInputType() { 1915 return InputType.TYPE_CLASS_TEXT; 1916 } 1917 1918 @Override 1919 protected char[] getAcceptedChars() { 1920 return DIGIT_CHARACTERS; 1921 } 1922 1923 @Override 1924 public CharSequence filter(CharSequence source, int start, int end, Spanned dest, 1925 int dstart, int dend) { 1926 if (mDisplayedValues == null) { 1927 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); 1928 if (filtered == null) { 1929 filtered = source.subSequence(start, end); 1930 } 1931 1932 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1933 + dest.subSequence(dend, dest.length()); 1934 1935 if ("".equals(result)) { 1936 return result; 1937 } 1938 int val = getSelectedPos(result); 1939 1940 /* 1941 * Ensure the user can't type in a value greater than the max 1942 * allowed. We have to allow less than min as the user might 1943 * want to delete some numbers and then type a new number. 1944 */ 1945 if (val > mMaxValue) { 1946 return ""; 1947 } else { 1948 return filtered; 1949 } 1950 } else { 1951 CharSequence filtered = String.valueOf(source.subSequence(start, end)); 1952 if (TextUtils.isEmpty(filtered)) { 1953 return ""; 1954 } 1955 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1956 + dest.subSequence(dend, dest.length()); 1957 String str = String.valueOf(result).toLowerCase(); 1958 for (String val : mDisplayedValues) { 1959 String valLowerCase = val.toLowerCase(); 1960 if (valLowerCase.startsWith(str)) { 1961 postSetSelectionCommand(result.length(), val.length()); 1962 return val.subSequence(dstart, val.length()); 1963 } 1964 } 1965 return ""; 1966 } 1967 } 1968 } 1969 1970 /** 1971 * Command for setting the input text selection. 1972 */ 1973 class SetSelectionCommand implements Runnable { 1974 private int mSelectionStart; 1975 1976 private int mSelectionEnd; 1977 1978 public void run() { 1979 mInputText.setSelection(mSelectionStart, mSelectionEnd); 1980 } 1981 } 1982 1983 /** 1984 * Command for adjusting the scroller to show in its center the closest of 1985 * the displayed items. 1986 */ 1987 class AdjustScrollerCommand implements Runnable { 1988 public void run() { 1989 mPreviousScrollerY = 0; 1990 if (mInitialScrollOffset == mCurrentScrollOffset) { 1991 updateInputTextView(); 1992 showInputControls(mShowInputControlsAnimimationDuration); 1993 return; 1994 } 1995 // adjust to the closest value 1996 int deltaY = mInitialScrollOffset - mCurrentScrollOffset; 1997 if (Math.abs(deltaY) > mSelectorElementHeight / 2) { 1998 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; 1999 } 2000 mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); 2001 invalidate(); 2002 } 2003 } 2004 2005 /** 2006 * Command for changing the current value from a long press by one. 2007 */ 2008 class ChangeCurrentByOneFromLongPressCommand implements Runnable { 2009 private boolean mIncrement; 2010 2011 private void setIncrement(boolean increment) { 2012 mIncrement = increment; 2013 } 2014 2015 public void run() { 2016 changeCurrentByOne(mIncrement); 2017 postDelayed(this, mLongPressUpdateInterval); 2018 } 2019 } 2020} 2021