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