NumberPicker.java revision 50f34d14f6dd3411fdbdb6a7b8b285c2b8fdbf5c
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 com.android.internal.R; 20 21import android.animation.Animator; 22import android.animation.AnimatorListenerAdapter; 23import android.animation.AnimatorSet; 24import android.animation.ObjectAnimator; 25import android.animation.ValueAnimator; 26import android.annotation.Widget; 27import android.content.Context; 28import android.content.res.ColorStateList; 29import android.content.res.TypedArray; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.Paint; 33import android.graphics.Paint.Align; 34import android.graphics.Rect; 35import android.text.InputFilter; 36import android.text.InputType; 37import android.text.Spanned; 38import android.text.TextUtils; 39import android.text.method.NumberKeyListener; 40import android.util.AttributeSet; 41import android.util.SparseArray; 42import android.view.KeyEvent; 43import android.view.LayoutInflater; 44import android.view.LayoutInflater.Filter; 45import android.view.MotionEvent; 46import android.view.VelocityTracker; 47import android.view.View; 48import android.view.ViewConfiguration; 49import android.view.animation.OvershootInterpolator; 50import android.view.inputmethod.InputMethodManager; 51 52/** 53 * A view for selecting a number For a dialog using this view, see 54 * {@link android.app.TimePickerDialog}. 55 * 56 * @hide 57 */ 58@Widget 59public class NumberPicker extends LinearLayout { 60 61 /** 62 * The index of the middle selector item. 63 */ 64 private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2; 65 66 /** 67 * The coefficient by which to adjust (divide) the max fling velocity. 68 */ 69 private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; 70 71 /** 72 * The the duration for adjusting the selector wheel. 73 */ 74 private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; 75 76 /** 77 * The the delay for showing the input controls after a single tap on the 78 * input text. 79 */ 80 private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration 81 .getDoubleTapTimeout(); 82 83 /** 84 * The update step for incrementing the current value. 85 */ 86 private static final int UPDATE_STEP_INCREMENT = 1; 87 88 /** 89 * The update step for decrementing the current value. 90 */ 91 private static final int UPDATE_STEP_DECREMENT = -1; 92 93 /** 94 * The strength of fading in the top and bottom while drawing the selector. 95 */ 96 private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; 97 98 /** 99 * The numbers accepted by the input text's {@link Filter} 100 */ 101 private static final char[] DIGIT_CHARACTERS = new char[] { 102 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 103 }; 104 105 /** 106 * Use a custom NumberPicker formatting callback to use two-digit minutes 107 * strings like "01". Keeping a static formatter etc. is the most efficient 108 * way to do this; it avoids creating temporary objects on every call to 109 * format(). 110 */ 111 public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() { 112 final StringBuilder mBuilder = new StringBuilder(); 113 114 final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US); 115 116 final Object[] mArgs = new Object[1]; 117 118 public String toString(int value) { 119 mArgs[0] = value; 120 mBuilder.delete(0, mBuilder.length()); 121 mFmt.format("%02d", mArgs); 122 return mFmt.toString(); 123 } 124 }; 125 126 /** 127 * The increment button. 128 */ 129 private final ImageButton mIncrementButton; 130 131 /** 132 * The decrement button. 133 */ 134 private final ImageButton mDecrementButton; 135 136 /** 137 * The text for showing the current value. 138 */ 139 private final EditText mInputText; 140 141 /** 142 * The height of the text. 143 */ 144 private final int mTextSize; 145 146 /** 147 * The values to be displayed instead the indices. 148 */ 149 private String[] mDisplayedValues; 150 151 /** 152 * Lower value of the range of numbers allowed for the NumberPicker 153 */ 154 private int mStart; 155 156 /** 157 * Upper value of the range of numbers allowed for the NumberPicker 158 */ 159 private int mEnd; 160 161 /** 162 * Current value of this NumberPicker 163 */ 164 private int mCurrent; 165 166 /** 167 * Listener to be notified upon current value change. 168 */ 169 private OnChangeListener mOnChangeListener; 170 171 /** 172 * Listener to be notified upon scroll state change. 173 */ 174 private OnScrollListener mOnScrollListener; 175 176 /** 177 * Formatter for for displaying the current value. 178 */ 179 private Formatter mFormatter; 180 181 /** 182 * The speed for updating the value form long press. 183 */ 184 private long mLongPressUpdateSpeed = 300; 185 186 /** 187 * Cache for the string representation of selector indices. 188 */ 189 private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>(); 190 191 /** 192 * The selector indices whose value are show by the selector. 193 */ 194 private final int[] mSelectorIndices = new int[] { 195 Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, 196 Integer.MIN_VALUE 197 }; 198 199 /** 200 * The {@link Paint} for drawing the selector. 201 */ 202 private final Paint mSelectorPaint; 203 204 /** 205 * The height of a selector element (text + gap). 206 */ 207 private int mSelectorElementHeight; 208 209 /** 210 * The initial offset of the scroll selector. 211 */ 212 private int mInitialScrollOffset = Integer.MIN_VALUE; 213 214 /** 215 * The current offset of the scroll selector. 216 */ 217 private int mCurrentScrollOffset; 218 219 /** 220 * The {@link Scroller} responsible for flinging the selector. 221 */ 222 private final Scroller mFlingScroller; 223 224 /** 225 * The {@link Scroller} responsible for adjusting the selector. 226 */ 227 private final Scroller mAdjustScroller; 228 229 /** 230 * The previous Y coordinate while scrolling the selector. 231 */ 232 private int mPreviousScrollerY; 233 234 /** 235 * Handle to the reusable command for setting the input text selection. 236 */ 237 private SetSelectionCommand mSetSelectionCommand; 238 239 /** 240 * Handle to the reusable command for adjusting the scroller. 241 */ 242 private AdjustScrollerCommand mAdjustScrollerCommand; 243 244 /** 245 * Handle to the reusable command for updating the current value from long 246 * press. 247 */ 248 private UpdateValueFromLongPressCommand mUpdateFromLongPressCommand; 249 250 /** 251 * {@link Animator} for showing the up/down arrows. 252 */ 253 private final AnimatorSet mShowInputControlsAnimator; 254 255 /** 256 * The Y position of the last down event. 257 */ 258 private float mLastDownEventY; 259 260 /** 261 * The Y position of the last motion event. 262 */ 263 private float mLastMotionEventY; 264 265 /** 266 * Flag if to begin edit on next up event. 267 */ 268 private boolean mBeginEditOnUpEvent; 269 270 /** 271 * Flag if to adjust the selector wheel on next up event. 272 */ 273 private boolean mAdjustScrollerOnUpEvent; 274 275 /** 276 * Flag if to draw the selector wheel. 277 */ 278 private boolean mDrawSelectorWheel; 279 280 /** 281 * Determines speed during touch scrolling. 282 */ 283 private VelocityTracker mVelocityTracker; 284 285 /** 286 * @see ViewConfiguration#getScaledTouchSlop() 287 */ 288 private int mTouchSlop; 289 290 /** 291 * @see ViewConfiguration#getScaledMinimumFlingVelocity() 292 */ 293 private int mMinimumFlingVelocity; 294 295 /** 296 * @see ViewConfiguration#getScaledMaximumFlingVelocity() 297 */ 298 private int mMaximumFlingVelocity; 299 300 /** 301 * Flag whether the selector should wrap around. 302 */ 303 private boolean mWrapSelector; 304 305 /** 306 * The back ground color used to optimize scroller fading. 307 */ 308 private final int mSolidColor; 309 310 /** 311 * Reusable {@link Rect} instance. 312 */ 313 private final Rect mTempRect = new Rect(); 314 315 /** 316 * The current scroll state of the number picker. 317 */ 318 private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; 319 320 /** 321 * The callback interface used to indicate the number value has changed. 322 */ 323 public interface OnChangeListener { 324 /** 325 * @param picker The NumberPicker associated with this listener. 326 * @param oldVal The previous value. 327 * @param newVal The new value. 328 */ 329 void onChanged(NumberPicker picker, int oldVal, int newVal); 330 } 331 332 /** 333 * Interface for listening to the picker scroll state. 334 */ 335 public interface OnScrollListener { 336 337 /** 338 * The view is not scrolling. 339 */ 340 public static int SCROLL_STATE_IDLE = 0; 341 342 /** 343 * The user is scrolling using touch, and their finger is still on the screen. 344 */ 345 public static int SCROLL_STATE_TOUCH_SCROLL = 1; 346 347 /** 348 * The user had previously been scrolling using touch and performed a fling. 349 */ 350 public static int SCROLL_STATE_FLING = 2; 351 352 /** 353 * Callback method to be invoked while the number picker is being scrolled. 354 * 355 * @param view The view whose scroll state is being reported 356 * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, 357 * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. 358 */ 359 public void onScrollStateChanged(NumberPicker view, int scrollState); 360 } 361 362 /** 363 * Interface used to format the number into a string for presentation 364 */ 365 public interface Formatter { 366 String toString(int value); 367 } 368 369 /** 370 * Create a new number picker 371 * 372 * @param context The application environment. 373 * @param attrs A collection of attributes. 374 */ 375 public NumberPicker(Context context, AttributeSet attrs) { 376 this(context, attrs, R.attr.numberPickerStyle); 377 } 378 379 /** 380 * Create a new number picker 381 * 382 * @param context the application environment. 383 * @param attrs a collection of attributes. 384 * @param defStyle The default style to apply to this view. 385 */ 386 public NumberPicker(Context context, AttributeSet attrs, int defStyle) { 387 super(context, attrs, defStyle); 388 389 // process style attributes 390 TypedArray attributesArray = context.obtainStyledAttributes(attrs, 391 R.styleable.NumberPicker, defStyle, 0); 392 int orientation = attributesArray.getInt(R.styleable.NumberPicker_orientation, VERTICAL); 393 setOrientation(orientation); 394 mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); 395 attributesArray.recycle(); 396 397 // By default Linearlayout that we extend is not drawn. This is 398 // its draw() method is not called but dispatchDraw() is called 399 // directly (see ViewGroup.drawChild()). However, this class uses 400 // the fading edge effect implemented by View and we need our 401 // draw() method to be called. Therefore, we declare we will draw. 402 setWillNotDraw(false); 403 setDrawSelectorWheel(false); 404 405 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 406 Context.LAYOUT_INFLATER_SERVICE); 407 inflater.inflate(R.layout.number_picker, this, true); 408 409 OnClickListener onClickListener = new OnClickListener() { 410 public void onClick(View v) { 411 mInputText.clearFocus(); 412 if (v.getId() == R.id.increment) { 413 changeCurrent(mCurrent + 1); 414 } else { 415 changeCurrent(mCurrent - 1); 416 } 417 } 418 }; 419 420 OnLongClickListener onLongClickListener = new OnLongClickListener() { 421 public boolean onLongClick(View v) { 422 mInputText.clearFocus(); 423 if (v.getId() == R.id.increment) { 424 postUpdateValueFromLongPress(UPDATE_STEP_INCREMENT); 425 } else { 426 postUpdateValueFromLongPress(UPDATE_STEP_DECREMENT); 427 } 428 return true; 429 } 430 }; 431 432 // increment button 433 mIncrementButton = (ImageButton) findViewById(R.id.increment); 434 mIncrementButton.setOnClickListener(onClickListener); 435 mIncrementButton.setOnLongClickListener(onLongClickListener); 436 437 // decrement button 438 mDecrementButton = (ImageButton) findViewById(R.id.decrement); 439 mDecrementButton.setOnClickListener(onClickListener); 440 mDecrementButton.setOnLongClickListener(onLongClickListener); 441 442 // input text 443 mInputText = (EditText) findViewById(R.id.timepicker_input); 444 mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { 445 public void onFocusChange(View v, boolean hasFocus) { 446 if (!hasFocus) { 447 validateInputTextView(v); 448 } 449 } 450 }); 451 mInputText.setFilters(new InputFilter[] { 452 new InputTextFilter() 453 }); 454 455 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 456 457 // initialize constants 458 mTouchSlop = ViewConfiguration.getTapTimeout(); 459 ViewConfiguration configuration = ViewConfiguration.get(context); 460 mTouchSlop = configuration.getScaledTouchSlop(); 461 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 462 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() 463 / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; 464 mTextSize = (int) mInputText.getTextSize(); 465 466 // create the selector wheel paint 467 Paint paint = new Paint(); 468 paint.setAntiAlias(true); 469 paint.setTextAlign(Align.CENTER); 470 paint.setTextSize(mTextSize); 471 paint.setTypeface(mInputText.getTypeface()); 472 ColorStateList colors = mInputText.getTextColors(); 473 int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); 474 paint.setColor(color); 475 mSelectorPaint = paint; 476 477 // create the animator for showing the input controls 478 final ValueAnimator fadeScroller = ObjectAnimator.ofInt(this, "selectorPaintAlpha", 255, 0); 479 final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton, 480 "alpha", 0, 1); 481 final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton, 482 "alpha", 0, 1); 483 mShowInputControlsAnimator = new AnimatorSet(); 484 mShowInputControlsAnimator.playTogether(fadeScroller, showIncrementButton, 485 showDecrementButton); 486 mShowInputControlsAnimator.setDuration(getResources().getInteger( 487 R.integer.config_longAnimTime)); 488 mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() { 489 private boolean mCanceled = false; 490 491 @Override 492 public void onAnimationEnd(Animator animation) { 493 if (!mCanceled) { 494 // if canceled => we still want the wheel drawn 495 setDrawSelectorWheel(false); 496 } 497 mCanceled = false; 498 mSelectorPaint.setAlpha(255); 499 invalidate(); 500 } 501 502 @Override 503 public void onAnimationCancel(Animator animation) { 504 if (mShowInputControlsAnimator.isRunning()) { 505 mCanceled = true; 506 } 507 } 508 }); 509 510 // create the fling and adjust scrollers 511 mFlingScroller = new Scroller(getContext()); 512 mAdjustScroller = new Scroller(getContext(), new OvershootInterpolator()); 513 514 updateInputTextView(); 515 updateIncrementAndDecrementButtonsVisibilityState(); 516 } 517 518 @Override 519 public void onWindowFocusChanged(boolean hasWindowFocus) { 520 super.onWindowFocusChanged(hasWindowFocus); 521 if (!hasWindowFocus) { 522 removeAllCallbacks(); 523 } 524 } 525 526 @Override 527 public boolean onInterceptTouchEvent(MotionEvent event) { 528 switch (event.getActionMasked()) { 529 case MotionEvent.ACTION_DOWN: 530 mLastMotionEventY = mLastDownEventY = event.getY(); 531 removeAllCallbacks(); 532 mBeginEditOnUpEvent = false; 533 mAdjustScrollerOnUpEvent = true; 534 if (mDrawSelectorWheel) { 535 boolean scrollersFinished = mFlingScroller.isFinished() 536 && mAdjustScroller.isFinished(); 537 if (!scrollersFinished) { 538 mFlingScroller.forceFinished(true); 539 mAdjustScroller.forceFinished(true); 540 tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE); 541 } 542 mBeginEditOnUpEvent = scrollersFinished; 543 mAdjustScrollerOnUpEvent = true; 544 hideInputControls(); 545 return true; 546 } 547 if (isEventInInputText(event)) { 548 mAdjustScrollerOnUpEvent = false; 549 setDrawSelectorWheel(true); 550 hideInputControls(); 551 return true; 552 } 553 break; 554 case MotionEvent.ACTION_MOVE: 555 float currentMoveY = event.getY(); 556 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); 557 if (deltaDownY > mTouchSlop) { 558 mBeginEditOnUpEvent = false; 559 tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 560 setDrawSelectorWheel(true); 561 hideInputControls(); 562 return true; 563 } 564 break; 565 } 566 return false; 567 } 568 569 @Override 570 public boolean onTouchEvent(MotionEvent ev) { 571 if (mVelocityTracker == null) { 572 mVelocityTracker = VelocityTracker.obtain(); 573 } 574 mVelocityTracker.addMovement(ev); 575 int action = ev.getActionMasked(); 576 switch (action) { 577 case MotionEvent.ACTION_MOVE: 578 float currentMoveY = ev.getY(); 579 if (mBeginEditOnUpEvent 580 || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 581 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); 582 if (deltaDownY > mTouchSlop) { 583 mBeginEditOnUpEvent = false; 584 tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 585 } 586 } 587 int deltaMoveY = (int) (currentMoveY - mLastMotionEventY); 588 scrollBy(0, deltaMoveY); 589 invalidate(); 590 mLastMotionEventY = currentMoveY; 591 break; 592 case MotionEvent.ACTION_UP: 593 if (mBeginEditOnUpEvent) { 594 setDrawSelectorWheel(false); 595 showInputControls(); 596 mInputText.requestFocus(); 597 InputMethodManager imm = (InputMethodManager) getContext().getSystemService( 598 Context.INPUT_METHOD_SERVICE); 599 imm.showSoftInput(mInputText, 0); 600 mInputText.setSelection(0, mInputText.getText().length()); 601 return true; 602 } 603 VelocityTracker velocityTracker = mVelocityTracker; 604 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 605 int initialVelocity = (int) velocityTracker.getYVelocity(); 606 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { 607 fling(initialVelocity); 608 tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_FLING); 609 } else { 610 if (mAdjustScrollerOnUpEvent) { 611 if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) { 612 postAdjustScrollerCommand(0); 613 } 614 } else { 615 postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS); 616 } 617 } 618 mVelocityTracker.recycle(); 619 mVelocityTracker = null; 620 break; 621 } 622 return true; 623 } 624 625 @Override 626 public boolean dispatchTouchEvent(MotionEvent event) { 627 int action = event.getActionMasked(); 628 if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) 629 && !isEventInInputText(event)) { 630 removeAllCallbacks(); 631 } 632 return super.dispatchTouchEvent(event); 633 } 634 635 @Override 636 public boolean dispatchKeyEvent(KeyEvent event) { 637 int keyCode = event.getKeyCode(); 638 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { 639 removeAllCallbacks(); 640 } 641 return super.dispatchKeyEvent(event); 642 } 643 644 @Override 645 public boolean dispatchTrackballEvent(MotionEvent event) { 646 int action = event.getActionMasked(); 647 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 648 removeAllCallbacks(); 649 } 650 return super.dispatchTrackballEvent(event); 651 } 652 653 @Override 654 public void computeScroll() { 655 if (!mDrawSelectorWheel) { 656 return; 657 } 658 Scroller scroller = mFlingScroller; 659 if (scroller.isFinished()) { 660 scroller = mAdjustScroller; 661 if (scroller.isFinished()) { 662 return; 663 } 664 } 665 scroller.computeScrollOffset(); 666 int currentScrollerY = scroller.getCurrY(); 667 if (mPreviousScrollerY == 0) { 668 mPreviousScrollerY = scroller.getStartY(); 669 } 670 scrollBy(0, currentScrollerY - mPreviousScrollerY); 671 mPreviousScrollerY = currentScrollerY; 672 if (scroller.isFinished()) { 673 onScrollerFinished(scroller); 674 } else { 675 invalidate(); 676 } 677 } 678 679 /** 680 * Set the enabled state of this view. The interpretation of the enabled 681 * state varies by subclass. 682 * 683 * @param enabled True if this view is enabled, false otherwise. 684 */ 685 @Override 686 public void setEnabled(boolean enabled) { 687 super.setEnabled(enabled); 688 mIncrementButton.setEnabled(enabled); 689 mDecrementButton.setEnabled(enabled); 690 mInputText.setEnabled(enabled); 691 } 692 693 /** 694 * Scrolls the selector with the given <code>vertical offset</code>. 695 */ 696 @Override 697 public void scrollBy(int x, int y) { 698 int[] selectorIndices = getSelectorIndices(); 699 if (mInitialScrollOffset == Integer.MIN_VALUE) { 700 int totalTextHeight = selectorIndices.length * mTextSize; 701 int totalTextGapHeight = (mBottom - mTop) - totalTextHeight; 702 int textGapCount = selectorIndices.length - 1; 703 int selectorTextGapHeight = totalTextGapHeight / textGapCount; 704 // compensate for integer division loss of the components used to 705 // calculate the text gap 706 int integerDivisionLoss = (mTextSize + mBottom - mTop) % textGapCount; 707 mInitialScrollOffset = mCurrentScrollOffset = mTextSize - integerDivisionLoss / 2; 708 mSelectorElementHeight = mTextSize + selectorTextGapHeight; 709 } 710 711 if (!mWrapSelector && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) { 712 mCurrentScrollOffset = mInitialScrollOffset; 713 return; 714 } 715 if (!mWrapSelector && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) { 716 mCurrentScrollOffset = mInitialScrollOffset; 717 return; 718 } 719 mCurrentScrollOffset += y; 720 while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight) { 721 mCurrentScrollOffset -= mSelectorElementHeight; 722 decrementSelectorIndices(selectorIndices); 723 changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); 724 if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) { 725 mCurrentScrollOffset = mInitialScrollOffset; 726 } 727 } 728 while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight) { 729 mCurrentScrollOffset += mSelectorElementHeight; 730 incrementScrollSelectorIndices(selectorIndices); 731 changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); 732 if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) { 733 mCurrentScrollOffset = mInitialScrollOffset; 734 } 735 } 736 } 737 738 /** 739 * Set the callback that indicates the number has been adjusted by the user. 740 * 741 * @param onChangeListener the callback, should not be null. 742 */ 743 public void setOnChangeListener(OnChangeListener onChangeListener) { 744 mOnChangeListener = onChangeListener; 745 } 746 747 /** 748 * Set the callback that in notified for scroll state changes. 749 * 750 * @param onScrollListener the callback, should not be null. 751 */ 752 public void setOnScrollListener(OnScrollListener onScrollListener) { 753 mOnScrollListener = onScrollListener; 754 } 755 756 /** 757 * Set the formatter that will be used to format the number for presentation 758 * 759 * @param formatter the formatter object. If formatter is null, 760 * String.valueOf() will be used 761 */ 762 public void setFormatter(Formatter formatter) { 763 mFormatter = formatter; 764 } 765 766 /** 767 * Set the range of numbers allowed for the number picker. The current value 768 * will be automatically set to the start. 769 * 770 * @param start the start of the range (inclusive) 771 * @param end the end of the range (inclusive) 772 */ 773 public void setRange(int start, int end) { 774 setRange(start, end, null); 775 } 776 777 /** 778 * Set the range of numbers allowed for the number picker. The current value 779 * will be automatically set to the start. Also provide a mapping for values 780 * used to display to the user. 781 * 782 * @param start the start of the range (inclusive) 783 * @param end the end of the range (inclusive) 784 * @param displayedValues the values displayed to the user. 785 */ 786 public void setRange(int start, int end, String[] displayedValues) { 787 boolean wrapSelector = (end - start) >= mSelectorIndices.length; 788 setRange(start, end, displayedValues, wrapSelector); 789 } 790 791 /** 792 * Set the range of numbers allowed for the number picker. The current value 793 * will be automatically set to the start. Also provide a mapping for values 794 * used to display to the user. 795 * 796 * @param start the start of the range (inclusive) 797 * @param end the end of the range (inclusive) 798 * @param displayedValues the values displayed to the user. 799 */ 800 public void setRange(int start, int end, String[] displayedValues, boolean wrapSelector) { 801 if (start < 0 || end < 0) { 802 throw new IllegalArgumentException("start and end must be > 0"); 803 } 804 805 mDisplayedValues = displayedValues; 806 mStart = start; 807 mEnd = end; 808 mCurrent = start; 809 810 setWrapSelector(wrapSelector); 811 updateInputTextView(); 812 813 if (displayedValues != null) { 814 // Allow text entry rather than strictly numeric entry. 815 mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT 816 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 817 } else { 818 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 819 } 820 821 // make sure cached string representations are dropped 822 mSelectorIndexToStringCache.clear(); 823 } 824 825 /** 826 * Set the current value for the number picker. 827 * 828 * @param current the current value the start of the range (inclusive) 829 * @throws IllegalArgumentException when current is not within the range of 830 * of the number picker 831 */ 832 public void setCurrent(int current) { 833 if (current < mStart || current > mEnd) { 834 throw new IllegalArgumentException("current should be >= start and <= end"); 835 } 836 mCurrent = current; 837 updateInputTextView(); 838 updateIncrementAndDecrementButtonsVisibilityState(); 839 } 840 841 /** 842 * Sets whether the selector shown during flinging/scrolling should wrap 843 * around the beginning and end values. 844 * 845 * @param wrapSelector Whether to wrap. 846 */ 847 public void setWrapSelector(boolean wrapSelector) { 848 if (wrapSelector && (mEnd - mStart) < mSelectorIndices.length) { 849 throw new IllegalStateException("Range less than selector items count."); 850 } 851 if (wrapSelector != mWrapSelector) { 852 // force the selector indices array to be reinitialized 853 mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE; 854 mWrapSelector = wrapSelector; 855 } 856 } 857 858 /** 859 * Sets the speed at which the numbers will scroll when the +/- buttons are 860 * longpressed 861 * 862 * @param speed The speed (in milliseconds) at which the numbers will scroll 863 * default 300ms 864 */ 865 public void setSpeed(long speed) { 866 mLongPressUpdateSpeed = speed; 867 } 868 869 /** 870 * Returns the current value of the NumberPicker 871 * 872 * @return the current value. 873 */ 874 public int getCurrent() { 875 return mCurrent; 876 } 877 878 @Override 879 public int getSolidColor() { 880 return mSolidColor; 881 } 882 883 @Override 884 protected float getTopFadingEdgeStrength() { 885 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 886 } 887 888 @Override 889 protected float getBottomFadingEdgeStrength() { 890 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 891 } 892 893 @Override 894 protected void onDetachedFromWindow() { 895 removeAllCallbacks(); 896 } 897 898 @Override 899 protected void dispatchDraw(Canvas canvas) { 900 // There is a good reason for doing this. See comments in draw(). 901 } 902 903 @Override 904 public void draw(Canvas canvas) { 905 // Dispatch draw to our children only if we are not currently running 906 // the animation for simultaneously fading out the scroll wheel and 907 // showing in the buttons. This class takes advantage of the View 908 // implementation of fading edges effect to draw the selector wheel. 909 // However, in View.draw(), the fading is applied after all the children 910 // have been drawn and we do not want this fading to be applied to the 911 // buttons which are currently showing in. Therefore, we draw our 912 // children 913 // after we have completed drawing ourselves. 914 915 // Draw the selector wheel if needed 916 if (mDrawSelectorWheel) { 917 super.draw(canvas); 918 } 919 920 // Draw our children if we are not showing the selector wheel of fading 921 // it out 922 if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) { 923 long drawTime = getDrawingTime(); 924 for (int i = 0, count = getChildCount(); i < count; i++) { 925 View child = getChildAt(i); 926 if (!child.isShown()) { 927 continue; 928 } 929 drawChild(canvas, getChildAt(i), drawTime); 930 } 931 } 932 } 933 934 @Override 935 protected void onDraw(Canvas canvas) { 936 // we only draw the selector wheel 937 if (!mDrawSelectorWheel) { 938 return; 939 } 940 float x = (mRight - mLeft) / 2; 941 float y = mCurrentScrollOffset; 942 943 int[] selectorIndices = getSelectorIndices(); 944 for (int i = 0; i < selectorIndices.length; i++) { 945 int selectorIndex = selectorIndices[i]; 946 String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); 947 canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint); 948 y += mSelectorElementHeight; 949 } 950 } 951 952 /** 953 * Returns the upper value of the range of the NumberPicker 954 * 955 * @return the uppper number of the range. 956 */ 957 protected int getEndRange() { 958 return mEnd; 959 } 960 961 /** 962 * Returns the lower value of the range of the NumberPicker 963 * 964 * @return the lower number of the range. 965 */ 966 protected int getBeginRange() { 967 return mStart; 968 } 969 970 /** 971 * Sets the current value of this NumberPicker, and sets mPrevious to the 972 * previous value. If current is greater than mEnd less than mStart, the 973 * value of mCurrent is wrapped around. Subclasses can override this to 974 * change the wrapping behavior 975 * 976 * @param current the new value of the NumberPicker 977 */ 978 private void changeCurrent(int current) { 979 if (mCurrent == current) { 980 return; 981 } 982 // Wrap around the values if we go past the start or end 983 if (mWrapSelector) { 984 current = getWrappedSelectorIndex(current); 985 } 986 int previous = mCurrent; 987 setCurrent(current); 988 notifyChange(previous, current); 989 } 990 991 /** 992 * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector 993 * wheel. 994 */ 995 @SuppressWarnings("unused") 996 // Called by ShowInputControlsAnimator via reflection 997 private void setSelectorPaintAlpha(int alpha) { 998 mSelectorPaint.setAlpha(alpha); 999 if (mDrawSelectorWheel) { 1000 invalidate(); 1001 } 1002 } 1003 1004 /** 1005 * @return If the <code>event</code> is in the input text. 1006 */ 1007 private boolean isEventInInputText(MotionEvent event) { 1008 mInputText.getHitRect(mTempRect); 1009 return mTempRect.contains((int) event.getX(), (int) event.getY()); 1010 } 1011 1012 /** 1013 * Sets if to <code>drawSelectionWheel</code>. 1014 */ 1015 private void setDrawSelectorWheel(boolean drawSelectorWheel) { 1016 mDrawSelectorWheel = drawSelectorWheel; 1017 // do not fade if the selector wheel not shown 1018 setVerticalFadingEdgeEnabled(drawSelectorWheel); 1019 } 1020 1021 /** 1022 * Callback invoked upon completion of a given <code>scroller</code>. 1023 */ 1024 private void onScrollerFinished(Scroller scroller) { 1025 if (scroller == mFlingScroller) { 1026 postAdjustScrollerCommand(0); 1027 tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE); 1028 } else { 1029 showInputControls(); 1030 updateInputTextView(); 1031 } 1032 } 1033 1034 /** 1035 * Notifies the scroll listener for the given <code>scrollState</code> 1036 * if the scroll state differs from the current scroll state. 1037 */ 1038 private void tryNotifyScrollListener(int scrollState) { 1039 if (mOnScrollListener != null && mScrollState != scrollState) { 1040 mScrollState = scrollState; 1041 mOnScrollListener.onScrollStateChanged(this, scrollState); 1042 } 1043 } 1044 1045 /** 1046 * Flings the selector with the given <code>velocityY</code>. 1047 */ 1048 private void fling(int velocityY) { 1049 mPreviousScrollerY = 0; 1050 Scroller flingScroller = mFlingScroller; 1051 1052 if (mWrapSelector) { 1053 if (velocityY > 0) { 1054 flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1055 } else { 1056 flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1057 } 1058 } else { 1059 if (velocityY > 0) { 1060 int maxY = mTextSize * (mCurrent - mStart); 1061 flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY); 1062 } else { 1063 int startY = mTextSize * (mEnd - mCurrent); 1064 int maxY = startY; 1065 flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY); 1066 } 1067 } 1068 1069 postAdjustScrollerCommand(flingScroller.getDuration()); 1070 invalidate(); 1071 } 1072 1073 /** 1074 * Hides the input controls which is the up/down arrows and the text field. 1075 */ 1076 private void hideInputControls() { 1077 mShowInputControlsAnimator.cancel(); 1078 mIncrementButton.setVisibility(INVISIBLE); 1079 mDecrementButton.setVisibility(INVISIBLE); 1080 mInputText.setVisibility(INVISIBLE); 1081 } 1082 1083 /** 1084 * Show the input controls by making them visible and animating the alpha 1085 * property up/down arrows. 1086 */ 1087 private void showInputControls() { 1088 updateIncrementAndDecrementButtonsVisibilityState(); 1089 mInputText.setVisibility(VISIBLE); 1090 mShowInputControlsAnimator.start(); 1091 } 1092 1093 /** 1094 * Updates the visibility state of the increment and decrement buttons. 1095 */ 1096 private void updateIncrementAndDecrementButtonsVisibilityState() { 1097 if (mWrapSelector || mCurrent < mEnd) { 1098 mIncrementButton.setVisibility(VISIBLE); 1099 } else { 1100 mIncrementButton.setVisibility(INVISIBLE); 1101 } 1102 if (mWrapSelector || mCurrent > mStart) { 1103 mDecrementButton.setVisibility(VISIBLE); 1104 } else { 1105 mDecrementButton.setVisibility(INVISIBLE); 1106 } 1107 } 1108 1109 /** 1110 * @return The selector indices array with proper values with the current as 1111 * the middle one. 1112 */ 1113 private int[] getSelectorIndices() { 1114 int current = getCurrent(); 1115 if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) { 1116 for (int i = 0; i < mSelectorIndices.length; i++) { 1117 int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); 1118 if (mWrapSelector) { 1119 selectorIndex = getWrappedSelectorIndex(selectorIndex); 1120 } 1121 mSelectorIndices[i] = selectorIndex; 1122 ensureCachedScrollSelectorValue(mSelectorIndices[i]); 1123 } 1124 } 1125 return mSelectorIndices; 1126 } 1127 1128 /** 1129 * @return The wrapped index <code>selectorIndex</code> value. 1130 * <p> 1131 * Note: The absolute value of the argument is never larger than 1132 * mEnd - mStart. 1133 * </p> 1134 */ 1135 private int getWrappedSelectorIndex(int selectorIndex) { 1136 if (selectorIndex > mEnd) { 1137 return (Math.abs(selectorIndex) - mEnd); 1138 } else if (selectorIndex < mStart) { 1139 return (mEnd - Math.abs(selectorIndex)); 1140 } 1141 return selectorIndex; 1142 } 1143 1144 /** 1145 * Increments the <code>selectorIndices</code> whose string representations 1146 * will be displayed in the selector. 1147 */ 1148 private void incrementScrollSelectorIndices(int[] selectorIndices) { 1149 for (int i = 0; i < selectorIndices.length - 1; i++) { 1150 selectorIndices[i] = selectorIndices[i + 1]; 1151 } 1152 int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; 1153 if (mWrapSelector && nextScrollSelectorIndex > mEnd) { 1154 nextScrollSelectorIndex = mStart; 1155 } 1156 selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; 1157 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1158 } 1159 1160 /** 1161 * Decrements the <code>selectorIndices</code> whose string representations 1162 * will be displayed in the selector. 1163 */ 1164 private void decrementSelectorIndices(int[] selectorIndices) { 1165 for (int i = selectorIndices.length - 1; i > 0; i--) { 1166 selectorIndices[i] = selectorIndices[i - 1]; 1167 } 1168 int nextScrollSelectorIndex = selectorIndices[1] - 1; 1169 if (mWrapSelector && nextScrollSelectorIndex < mStart) { 1170 nextScrollSelectorIndex = mEnd; 1171 } 1172 selectorIndices[0] = nextScrollSelectorIndex; 1173 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1174 } 1175 1176 /** 1177 * Ensures we have a cached string representation of the given <code> 1178 * selectorIndex</code> 1179 * to avoid multiple instantiations of the same string. 1180 */ 1181 private void ensureCachedScrollSelectorValue(int selectorIndex) { 1182 SparseArray<String> cache = mSelectorIndexToStringCache; 1183 String scrollSelectorValue = cache.get(selectorIndex); 1184 if (scrollSelectorValue != null) { 1185 return; 1186 } 1187 if (selectorIndex < mStart || selectorIndex > mEnd) { 1188 scrollSelectorValue = ""; 1189 } else { 1190 if (mDisplayedValues != null) { 1191 int displayedValueIndex = selectorIndex - mStart; 1192 scrollSelectorValue = mDisplayedValues[displayedValueIndex]; 1193 } else { 1194 scrollSelectorValue = formatNumber(selectorIndex); 1195 } 1196 } 1197 cache.put(selectorIndex, scrollSelectorValue); 1198 } 1199 1200 private String formatNumber(int value) { 1201 return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value); 1202 } 1203 1204 private void validateInputTextView(View v) { 1205 String str = String.valueOf(((TextView) v).getText()); 1206 if (TextUtils.isEmpty(str)) { 1207 // Restore to the old value as we don't allow empty values 1208 updateInputTextView(); 1209 } else { 1210 // Check the new value and ensure it's in range 1211 int current = getSelectedPos(str.toString()); 1212 changeCurrent(current); 1213 } 1214 } 1215 1216 /** 1217 * Updates the view of this NumberPicker. If displayValues were specified in 1218 * {@link #setRange}, the string corresponding to the index specified by the 1219 * current value will be returned. Otherwise, the formatter specified in 1220 * {@link #setFormatter} will be used to format the number. 1221 */ 1222 private void updateInputTextView() { 1223 /* 1224 * If we don't have displayed values then use the current number else 1225 * find the correct value in the displayed values for the current 1226 * number. 1227 */ 1228 if (mDisplayedValues == null) { 1229 mInputText.setText(formatNumber(mCurrent)); 1230 } else { 1231 mInputText.setText(mDisplayedValues[mCurrent - mStart]); 1232 } 1233 mInputText.setSelection(mInputText.getText().length()); 1234 } 1235 1236 /** 1237 * Notifies the listener, if registered, of a change of the value of this 1238 * NumberPicker. 1239 */ 1240 private void notifyChange(int previous, int current) { 1241 if (mOnChangeListener != null) { 1242 mOnChangeListener.onChanged(this, previous, mCurrent); 1243 } 1244 } 1245 1246 /** 1247 * Posts a command for updating the current value every <code>updateMillis 1248 * </code>. 1249 */ 1250 private void postUpdateValueFromLongPress(int updateMillis) { 1251 mInputText.clearFocus(); 1252 removeAllCallbacks(); 1253 if (mUpdateFromLongPressCommand == null) { 1254 mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand(); 1255 } 1256 mUpdateFromLongPressCommand.setUpdateStep(updateMillis); 1257 post(mUpdateFromLongPressCommand); 1258 } 1259 1260 /** 1261 * Removes all pending callback from the message queue. 1262 */ 1263 private void removeAllCallbacks() { 1264 if (mUpdateFromLongPressCommand != null) { 1265 removeCallbacks(mUpdateFromLongPressCommand); 1266 } 1267 if (mAdjustScrollerCommand != null) { 1268 removeCallbacks(mAdjustScrollerCommand); 1269 } 1270 if (mSetSelectionCommand != null) { 1271 removeCallbacks(mSetSelectionCommand); 1272 } 1273 } 1274 1275 /** 1276 * @return The selected index given its displayed <code>value</code>. 1277 */ 1278 private int getSelectedPos(String value) { 1279 if (mDisplayedValues == null) { 1280 try { 1281 return Integer.parseInt(value); 1282 } catch (NumberFormatException e) { 1283 // Ignore as if it's not a number we don't care 1284 } 1285 } else { 1286 for (int i = 0; i < mDisplayedValues.length; i++) { 1287 // Don't force the user to type in jan when ja will do 1288 value = value.toLowerCase(); 1289 if (mDisplayedValues[i].toLowerCase().startsWith(value)) { 1290 return mStart + i; 1291 } 1292 } 1293 1294 /* 1295 * The user might have typed in a number into the month field i.e. 1296 * 10 instead of OCT so support that too. 1297 */ 1298 try { 1299 return Integer.parseInt(value); 1300 } catch (NumberFormatException e) { 1301 1302 // Ignore as if it's not a number we don't care 1303 } 1304 } 1305 return mStart; 1306 } 1307 1308 /** 1309 * Posts an {@link SetSelectionCommand} from the given <code>selectionStart 1310 * </code> to 1311 * <code>selectionEnd</code>. 1312 */ 1313 private void postSetSelectionCommand(int selectionStart, int selectionEnd) { 1314 if (mSetSelectionCommand == null) { 1315 mSetSelectionCommand = new SetSelectionCommand(); 1316 } else { 1317 removeCallbacks(mSetSelectionCommand); 1318 } 1319 mSetSelectionCommand.mSelectionStart = selectionStart; 1320 mSetSelectionCommand.mSelectionEnd = selectionEnd; 1321 post(mSetSelectionCommand); 1322 } 1323 1324 /** 1325 * Posts an {@link AdjustScrollerCommand} within the given <code> 1326 * delayMillis</code> 1327 * . 1328 */ 1329 private void postAdjustScrollerCommand(int delayMillis) { 1330 if (mAdjustScrollerCommand == null) { 1331 mAdjustScrollerCommand = new AdjustScrollerCommand(); 1332 } else { 1333 removeCallbacks(mAdjustScrollerCommand); 1334 } 1335 postDelayed(mAdjustScrollerCommand, delayMillis); 1336 } 1337 1338 /** 1339 * Filter for accepting only valid indices or prefixes of the string 1340 * representation of valid indices. 1341 */ 1342 class InputTextFilter extends NumberKeyListener { 1343 1344 // XXX This doesn't allow for range limits when controlled by a 1345 // soft input method! 1346 public int getInputType() { 1347 return InputType.TYPE_CLASS_TEXT; 1348 } 1349 1350 @Override 1351 protected char[] getAcceptedChars() { 1352 return DIGIT_CHARACTERS; 1353 } 1354 1355 @Override 1356 public CharSequence filter(CharSequence source, int start, int end, Spanned dest, 1357 int dstart, int dend) { 1358 if (mDisplayedValues == null) { 1359 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); 1360 if (filtered == null) { 1361 filtered = source.subSequence(start, end); 1362 } 1363 1364 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1365 + dest.subSequence(dend, dest.length()); 1366 1367 if ("".equals(result)) { 1368 return result; 1369 } 1370 int val = getSelectedPos(result); 1371 1372 /* 1373 * Ensure the user can't type in a value greater than the max 1374 * allowed. We have to allow less than min as the user might 1375 * want to delete some numbers and then type a new number. 1376 */ 1377 if (val > mEnd) { 1378 return ""; 1379 } else { 1380 return filtered; 1381 } 1382 } else { 1383 CharSequence filtered = String.valueOf(source.subSequence(start, end)); 1384 if (TextUtils.isEmpty(filtered)) { 1385 return ""; 1386 } 1387 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1388 + dest.subSequence(dend, dest.length()); 1389 String str = String.valueOf(result).toLowerCase(); 1390 for (String val : mDisplayedValues) { 1391 String valLowerCase = val.toLowerCase(); 1392 if (valLowerCase.startsWith(str)) { 1393 postSetSelectionCommand(result.length(), val.length()); 1394 return val.subSequence(dstart, val.length()); 1395 } 1396 } 1397 return ""; 1398 } 1399 } 1400 } 1401 1402 /** 1403 * Command for setting the input text selection. 1404 */ 1405 class SetSelectionCommand implements Runnable { 1406 private int mSelectionStart; 1407 1408 private int mSelectionEnd; 1409 1410 public void run() { 1411 mInputText.setSelection(mSelectionStart, mSelectionEnd); 1412 } 1413 } 1414 1415 /** 1416 * Command for adjusting the scroller to show in its center the closest of 1417 * the displayed items. 1418 */ 1419 class AdjustScrollerCommand implements Runnable { 1420 public void run() { 1421 mPreviousScrollerY = 0; 1422 int deltaY = mInitialScrollOffset - mCurrentScrollOffset; 1423 float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize; 1424 int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS); 1425 mAdjustScroller.startScroll(0, 0, 0, deltaY, duration); 1426 invalidate(); 1427 } 1428 } 1429 1430 /** 1431 * Command for updating the current value from a long press. 1432 */ 1433 class UpdateValueFromLongPressCommand implements Runnable { 1434 private int mUpdateStep = 0; 1435 1436 private void setUpdateStep(int updateStep) { 1437 mUpdateStep = updateStep; 1438 } 1439 1440 public void run() { 1441 changeCurrent(mCurrent + mUpdateStep); 1442 postDelayed(this, mLongPressUpdateSpeed); 1443 } 1444 } 1445} 1446