NumberPicker.java revision 012dd5a461f18a2e5dad38c60282fac1c21ff7fe
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.numberpicker_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 747 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 748 mCurrentScrollOffset = mInitialScrollOffset; 749 return; 750 } 751 if (!mWrapSelectorWheel && y < 0 752 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 753 mCurrentScrollOffset = mInitialScrollOffset; 754 return; 755 } 756 mCurrentScrollOffset += y; 757 while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight) { 758 mCurrentScrollOffset -= mSelectorElementHeight; 759 decrementSelectorIndices(selectorIndices); 760 changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); 761 if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 762 mCurrentScrollOffset = mInitialScrollOffset; 763 } 764 } 765 while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight) { 766 mCurrentScrollOffset += mSelectorElementHeight; 767 incrementScrollSelectorIndices(selectorIndices); 768 changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); 769 if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 770 mCurrentScrollOffset = mInitialScrollOffset; 771 } 772 } 773 } 774 775 @Override 776 public int getSolidColor() { 777 return mSolidColor; 778 } 779 780 /** 781 * Sets the listener to be notified on change of the current value. 782 * 783 * @param onValueChangedListener The listener. 784 */ 785 public void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) { 786 mOnValueChangedListener = onValueChangedListener; 787 } 788 789 /** 790 * Set listener to be notified for scroll state changes. 791 * 792 * @param onScrollListener The listener. 793 */ 794 public void setOnScrollListener(OnScrollListener onScrollListener) { 795 mOnScrollListener = onScrollListener; 796 } 797 798 /** 799 * Set the formatter to be used for formatting the current value. 800 * <p> 801 * Note: If you have provided alternative values for the values this 802 * formatter is never invoked. 803 * </p> 804 * 805 * @param formatter The formatter object. If formatter is <code>null</code>, 806 * {@link String#valueOf(int)} will be used. 807 * 808 * @see #setDisplayedValues(String[]) 809 */ 810 public void setFormatter(Formatter formatter) { 811 if (formatter == mFormatter) { 812 return; 813 } 814 mFormatter = formatter; 815 resetSelectorWheelIndices(); 816 } 817 818 /** 819 * Set the current value for the number picker. 820 * <p> 821 * If the argument is less than the {@link NumberPicker#getMinValue()} and 822 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 823 * current value is set to the {@link NumberPicker#getMinValue()} value. 824 * </p> 825 * <p> 826 * If the argument is less than the {@link NumberPicker#getMinValue()} and 827 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 828 * current value is set to the {@link NumberPicker#getMaxValue()} value. 829 * </p> 830 * <p> 831 * If the argument is less than the {@link NumberPicker#getMaxValue()} and 832 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 833 * current value is set to the {@link NumberPicker#getMaxValue()} value. 834 * </p> 835 * <p> 836 * If the argument is less than the {@link NumberPicker#getMaxValue()} and 837 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 838 * current value is set to the {@link NumberPicker#getMinValue()} value. 839 * </p> 840 * 841 * @param value The current value. 842 * @see #setWrapSelectorWheel(boolean) 843 * @see #setMinValue(int) 844 * @see #setMaxValue(int) 845 */ 846 public void setValue(int value) { 847 if (mValue == value) { 848 return; 849 } 850 if (value < mMinValue) { 851 value = mWrapSelectorWheel ? mMaxValue : mMinValue; 852 } 853 if (value > mMaxValue) { 854 value = mWrapSelectorWheel ? mMinValue : mMaxValue; 855 } 856 mValue = value; 857 updateInputTextView(); 858 updateIncrementAndDecrementButtonsVisibilityState(); 859 } 860 861 /** 862 * Gets whether the selector wheel wraps when reaching the min/max value. 863 * 864 * @return True if the selector wheel wraps. 865 * 866 * @see #getMinValue() 867 * @see #getMaxValue() 868 */ 869 public boolean getWrapSelectorWheel() { 870 return mWrapSelectorWheel; 871 } 872 873 /** 874 * Sets whether the selector wheel shown during flinging/scrolling should 875 * wrap around the {@link NumberPicker#getMinValue()} and 876 * {@link NumberPicker#getMaxValue()} values. 877 * <p> 878 * By default if the range (max - min) is more than five (the number of 879 * items shown on the selector wheel) the selector wheel wrapping is 880 * enabled. 881 * </p> 882 * 883 * @param wrapSelector Whether to wrap. 884 */ 885 public void setWrapSelectorWheel(boolean wrapSelector) { 886 if (wrapSelector && (mMaxValue - mMinValue) < mSelectorIndices.length) { 887 throw new IllegalStateException("Range less than selector items count."); 888 } 889 if (wrapSelector != mWrapSelectorWheel) { 890 // force the selector indices array to be reinitialized 891 mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE; 892 mWrapSelectorWheel = wrapSelector; 893 } 894 } 895 896 /** 897 * Sets the speed at which the numbers be incremented and decremented when 898 * the up and down buttons are long pressed respectively. 899 * <p> 900 * The default value is 300 ms. 901 * </p> 902 * 903 * @param intervalMillis The speed (in milliseconds) at which the numbers 904 * will be incremented and decremented. 905 */ 906 public void setOnLongPressUpdateInterval(long intervalMillis) { 907 mLongPressUpdateInterval = intervalMillis; 908 } 909 910 /** 911 * Returns the value of the picker. 912 * 913 * @return The value. 914 */ 915 public int getValue() { 916 return mValue; 917 } 918 919 /** 920 * Returns the min value of the picker. 921 * 922 * @return The min value 923 */ 924 public int getMinValue() { 925 return mMinValue; 926 } 927 928 /** 929 * Sets the min value of the picker. 930 * 931 * @param minValue The min value. 932 */ 933 public void setMinValue(int minValue) { 934 if (mMinValue == minValue) { 935 return; 936 } 937 if (minValue < 0) { 938 throw new IllegalArgumentException("minValue must be >= 0"); 939 } 940 mMinValue = minValue; 941 if (mMinValue > mValue) { 942 mValue = mMinValue; 943 } 944 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; 945 setWrapSelectorWheel(wrapSelectorWheel); 946 resetSelectorWheelIndices(); 947 updateInputTextView(); 948 updateIncrementAndDecrementButtonsVisibilityState(); 949 } 950 951 /** 952 * Returns the max value of the picker. 953 * 954 * @return The max value. 955 */ 956 public int getMaxValue() { 957 return mMaxValue; 958 } 959 960 /** 961 * Sets the max value of the picker. 962 * 963 * @param maxValue The max value. 964 */ 965 public void setMaxValue(int maxValue) { 966 if (mMaxValue == maxValue) { 967 return; 968 } 969 if (maxValue < 0) { 970 throw new IllegalArgumentException("maxValue must be >= 0"); 971 } 972 mMaxValue = maxValue; 973 if (mMaxValue < mValue) { 974 mValue = mMaxValue; 975 } 976 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; 977 setWrapSelectorWheel(wrapSelectorWheel); 978 resetSelectorWheelIndices(); 979 updateInputTextView(); 980 updateIncrementAndDecrementButtonsVisibilityState(); 981 } 982 983 /** 984 * Gets the values to be displayed instead of string values. 985 * 986 * @return The displayed values. 987 */ 988 public String[] getDisplayedValues() { 989 return mDisplayedValues; 990 } 991 992 /** 993 * Sets the values to be displayed. 994 * 995 * @param displayedValues The displayed values. 996 */ 997 public void setDisplayedValues(String[] displayedValues) { 998 if (mDisplayedValues == displayedValues) { 999 return; 1000 } 1001 mDisplayedValues = displayedValues; 1002 if (mDisplayedValues != null) { 1003 // Allow text entry rather than strictly numeric entry. 1004 mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT 1005 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 1006 } else { 1007 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 1008 } 1009 updateInputTextView(); 1010 resetSelectorWheelIndices(); 1011 } 1012 1013 @Override 1014 protected float getTopFadingEdgeStrength() { 1015 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1016 } 1017 1018 @Override 1019 protected float getBottomFadingEdgeStrength() { 1020 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1021 } 1022 1023 @Override 1024 protected void onDetachedFromWindow() { 1025 removeAllCallbacks(); 1026 } 1027 1028 @Override 1029 protected void dispatchDraw(Canvas canvas) { 1030 // There is a good reason for doing this. See comments in draw(). 1031 } 1032 1033 @Override 1034 public void draw(Canvas canvas) { 1035 // Dispatch draw to our children only if we are not currently running 1036 // the animation for simultaneously fading out the scroll wheel and 1037 // showing in the buttons. This class takes advantage of the View 1038 // implementation of fading edges effect to draw the selector wheel. 1039 // However, in View.draw(), the fading is applied after all the children 1040 // have been drawn and we do not want this fading to be applied to the 1041 // buttons which are currently showing in. Therefore, we draw our 1042 // children 1043 // after we have completed drawing ourselves. 1044 1045 super.draw(canvas); 1046 1047 // Draw our children if we are not showing the selector wheel of fading 1048 // it out 1049 if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) { 1050 long drawTime = getDrawingTime(); 1051 for (int i = 0, count = getChildCount(); i < count; i++) { 1052 View child = getChildAt(i); 1053 if (!child.isShown()) { 1054 continue; 1055 } 1056 drawChild(canvas, getChildAt(i), drawTime); 1057 } 1058 } 1059 } 1060 1061 @Override 1062 protected void onDraw(Canvas canvas) { 1063 // we only draw the selector wheel 1064 if (!mDrawSelectorWheel) { 1065 return; 1066 } 1067 float x = (mRight - mLeft) / 2; 1068 float y = mCurrentScrollOffset; 1069 1070 int[] selectorIndices = getSelectorIndices(); 1071 for (int i = 0; i < selectorIndices.length; i++) { 1072 int selectorIndex = selectorIndices[i]; 1073 String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); 1074 canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint); 1075 y += mSelectorElementHeight; 1076 } 1077 } 1078 1079 /** 1080 * Resets the selector indices and clear the cached 1081 * string representation of these indices. 1082 */ 1083 private void resetSelectorWheelIndices() { 1084 mSelectorIndexToStringCache.clear(); 1085 int[] selectorIdices = getSelectorIndices(); 1086 for (int i = 0; i < selectorIdices.length; i++) { 1087 selectorIdices[i] = Integer.MIN_VALUE; 1088 } 1089 } 1090 1091 /** 1092 * Sets the current value of this NumberPicker, and sets mPrevious to the 1093 * previous value. If current is greater than mEnd less than mStart, the 1094 * value of mCurrent is wrapped around. Subclasses can override this to 1095 * change the wrapping behavior 1096 * 1097 * @param current the new value of the NumberPicker 1098 */ 1099 private void changeCurrent(int current) { 1100 if (mValue == current) { 1101 return; 1102 } 1103 // Wrap around the values if we go past the start or end 1104 if (mWrapSelectorWheel) { 1105 current = getWrappedSelectorIndex(current); 1106 } 1107 int previous = mValue; 1108 setValue(current); 1109 notifyChange(previous, current); 1110 } 1111 1112 /** 1113 * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector 1114 * wheel. 1115 */ 1116 @SuppressWarnings("unused") 1117 // Called by ShowInputControlsAnimator via reflection 1118 private void setSelectorPaintAlpha(int alpha) { 1119 mSelectorPaint.setAlpha(alpha); 1120 if (mDrawSelectorWheel) { 1121 invalidate(); 1122 } 1123 } 1124 1125 /** 1126 * @return If the <code>event</code> is in the input text. 1127 */ 1128 private boolean isEventInInputText(MotionEvent event) { 1129 mInputText.getHitRect(mTempRect); 1130 return mTempRect.contains((int) event.getX(), (int) event.getY()); 1131 } 1132 1133 /** 1134 * Sets if to <code>drawSelectionWheel</code>. 1135 */ 1136 private void setDrawSelectorWheel(boolean drawSelectorWheel) { 1137 mDrawSelectorWheel = drawSelectorWheel; 1138 // do not fade if the selector wheel not shown 1139 setVerticalFadingEdgeEnabled(drawSelectorWheel); 1140 } 1141 1142 /** 1143 * Callback invoked upon completion of a given <code>scroller</code>. 1144 */ 1145 private void onScrollerFinished(Scroller scroller) { 1146 if (scroller == mFlingScroller) { 1147 postAdjustScrollerCommand(0); 1148 tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE); 1149 } else { 1150 showInputControls(); 1151 updateInputTextView(); 1152 } 1153 } 1154 1155 /** 1156 * Notifies the scroll listener for the given <code>scrollState</code> 1157 * if the scroll state differs from the current scroll state. 1158 */ 1159 private void tryNotifyScrollListener(int scrollState) { 1160 if (mOnScrollListener != null && mScrollState != scrollState) { 1161 mScrollState = scrollState; 1162 mOnScrollListener.onScrollStateChange(this, scrollState); 1163 } 1164 } 1165 1166 /** 1167 * Flings the selector with the given <code>velocityY</code>. 1168 */ 1169 private void fling(int velocityY) { 1170 mPreviousScrollerY = 0; 1171 Scroller flingScroller = mFlingScroller; 1172 1173 if (mWrapSelectorWheel) { 1174 if (velocityY > 0) { 1175 flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1176 } else { 1177 flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1178 } 1179 } else { 1180 if (velocityY > 0) { 1181 int maxY = mTextSize * (mValue - mMinValue); 1182 flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY); 1183 } else { 1184 int startY = mTextSize * (mMaxValue - mValue); 1185 int maxY = startY; 1186 flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY); 1187 } 1188 } 1189 1190 postAdjustScrollerCommand(flingScroller.getDuration()); 1191 invalidate(); 1192 } 1193 1194 /** 1195 * Hides the input controls which is the up/down arrows and the text field. 1196 */ 1197 private void hideInputControls() { 1198 mShowInputControlsAnimator.cancel(); 1199 mIncrementButton.setVisibility(INVISIBLE); 1200 mDecrementButton.setVisibility(INVISIBLE); 1201 mInputText.setVisibility(INVISIBLE); 1202 } 1203 1204 /** 1205 * Show the input controls by making them visible and animating the alpha 1206 * property up/down arrows. 1207 */ 1208 private void showInputControls() { 1209 updateIncrementAndDecrementButtonsVisibilityState(); 1210 mInputText.setVisibility(VISIBLE); 1211 mShowInputControlsAnimator.start(); 1212 } 1213 1214 /** 1215 * Updates the visibility state of the increment and decrement buttons. 1216 */ 1217 private void updateIncrementAndDecrementButtonsVisibilityState() { 1218 if (mWrapSelectorWheel || mValue < mMaxValue) { 1219 mIncrementButton.setVisibility(VISIBLE); 1220 } else { 1221 mIncrementButton.setVisibility(INVISIBLE); 1222 } 1223 if (mWrapSelectorWheel || mValue > mMinValue) { 1224 mDecrementButton.setVisibility(VISIBLE); 1225 } else { 1226 mDecrementButton.setVisibility(INVISIBLE); 1227 } 1228 } 1229 1230 /** 1231 * @return The selector indices array with proper values with the current as 1232 * the middle one. 1233 */ 1234 private int[] getSelectorIndices() { 1235 int current = getValue(); 1236 if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) { 1237 for (int i = 0; i < mSelectorIndices.length; i++) { 1238 int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); 1239 if (mWrapSelectorWheel) { 1240 selectorIndex = getWrappedSelectorIndex(selectorIndex); 1241 } 1242 mSelectorIndices[i] = selectorIndex; 1243 ensureCachedScrollSelectorValue(mSelectorIndices[i]); 1244 } 1245 } 1246 return mSelectorIndices; 1247 } 1248 1249 /** 1250 * @return The wrapped index <code>selectorIndex</code> value. 1251 */ 1252 private int getWrappedSelectorIndex(int selectorIndex) { 1253 if (selectorIndex > mMaxValue) { 1254 return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1; 1255 } else if (selectorIndex < mMinValue) { 1256 return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1; 1257 } 1258 return selectorIndex; 1259 } 1260 1261 /** 1262 * Increments the <code>selectorIndices</code> whose string representations 1263 * will be displayed in the selector. 1264 */ 1265 private void incrementScrollSelectorIndices(int[] selectorIndices) { 1266 for (int i = 0; i < selectorIndices.length - 1; i++) { 1267 selectorIndices[i] = selectorIndices[i + 1]; 1268 } 1269 int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; 1270 if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) { 1271 nextScrollSelectorIndex = mMinValue; 1272 } 1273 selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; 1274 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1275 } 1276 1277 /** 1278 * Decrements the <code>selectorIndices</code> whose string representations 1279 * will be displayed in the selector. 1280 */ 1281 private void decrementSelectorIndices(int[] selectorIndices) { 1282 for (int i = selectorIndices.length - 1; i > 0; i--) { 1283 selectorIndices[i] = selectorIndices[i - 1]; 1284 } 1285 int nextScrollSelectorIndex = selectorIndices[1] - 1; 1286 if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) { 1287 nextScrollSelectorIndex = mMaxValue; 1288 } 1289 selectorIndices[0] = nextScrollSelectorIndex; 1290 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1291 } 1292 1293 /** 1294 * Ensures we have a cached string representation of the given <code> 1295 * selectorIndex</code> 1296 * to avoid multiple instantiations of the same string. 1297 */ 1298 private void ensureCachedScrollSelectorValue(int selectorIndex) { 1299 SparseArray<String> cache = mSelectorIndexToStringCache; 1300 String scrollSelectorValue = cache.get(selectorIndex); 1301 if (scrollSelectorValue != null) { 1302 return; 1303 } 1304 if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { 1305 scrollSelectorValue = ""; 1306 } else { 1307 if (mDisplayedValues != null) { 1308 int displayedValueIndex = selectorIndex - mMinValue; 1309 scrollSelectorValue = mDisplayedValues[displayedValueIndex]; 1310 } else { 1311 scrollSelectorValue = formatNumber(selectorIndex); 1312 } 1313 } 1314 cache.put(selectorIndex, scrollSelectorValue); 1315 } 1316 1317 private String formatNumber(int value) { 1318 return (mFormatter != null) ? mFormatter.format(value) : String.valueOf(value); 1319 } 1320 1321 private void validateInputTextView(View v) { 1322 String str = String.valueOf(((TextView) v).getText()); 1323 if (TextUtils.isEmpty(str)) { 1324 // Restore to the old value as we don't allow empty values 1325 updateInputTextView(); 1326 } else { 1327 // Check the new value and ensure it's in range 1328 int current = getSelectedPos(str.toString()); 1329 changeCurrent(current); 1330 } 1331 } 1332 1333 /** 1334 * Updates the view of this NumberPicker. If displayValues were specified in 1335 * {@link #setRange}, the string corresponding to the index specified by the 1336 * current value will be returned. Otherwise, the formatter specified in 1337 * {@link #setFormatter} will be used to format the number. 1338 */ 1339 private void updateInputTextView() { 1340 /* 1341 * If we don't have displayed values then use the current number else 1342 * find the correct value in the displayed values for the current 1343 * number. 1344 */ 1345 if (mDisplayedValues == null) { 1346 mInputText.setText(formatNumber(mValue)); 1347 } else { 1348 mInputText.setText(mDisplayedValues[mValue - mMinValue]); 1349 } 1350 mInputText.setSelection(mInputText.getText().length()); 1351 } 1352 1353 /** 1354 * Notifies the listener, if registered, of a change of the value of this 1355 * NumberPicker. 1356 */ 1357 private void notifyChange(int previous, int current) { 1358 if (mOnValueChangedListener != null) { 1359 mOnValueChangedListener.onValueChange(this, previous, mValue); 1360 } 1361 } 1362 1363 /** 1364 * Posts a command for updating the current value every <code>updateMillis 1365 * </code>. 1366 */ 1367 private void postUpdateValueFromLongPress(int updateMillis) { 1368 mInputText.clearFocus(); 1369 removeAllCallbacks(); 1370 if (mUpdateFromLongPressCommand == null) { 1371 mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand(); 1372 } 1373 mUpdateFromLongPressCommand.setUpdateStep(updateMillis); 1374 post(mUpdateFromLongPressCommand); 1375 } 1376 1377 /** 1378 * Removes all pending callback from the message queue. 1379 */ 1380 private void removeAllCallbacks() { 1381 if (mUpdateFromLongPressCommand != null) { 1382 removeCallbacks(mUpdateFromLongPressCommand); 1383 } 1384 if (mAdjustScrollerCommand != null) { 1385 removeCallbacks(mAdjustScrollerCommand); 1386 } 1387 if (mSetSelectionCommand != null) { 1388 removeCallbacks(mSetSelectionCommand); 1389 } 1390 } 1391 1392 /** 1393 * @return The selected index given its displayed <code>value</code>. 1394 */ 1395 private int getSelectedPos(String value) { 1396 if (mDisplayedValues == null) { 1397 try { 1398 return Integer.parseInt(value); 1399 } catch (NumberFormatException e) { 1400 // Ignore as if it's not a number we don't care 1401 } 1402 } else { 1403 for (int i = 0; i < mDisplayedValues.length; i++) { 1404 // Don't force the user to type in jan when ja will do 1405 value = value.toLowerCase(); 1406 if (mDisplayedValues[i].toLowerCase().startsWith(value)) { 1407 return mMinValue + i; 1408 } 1409 } 1410 1411 /* 1412 * The user might have typed in a number into the month field i.e. 1413 * 10 instead of OCT so support that too. 1414 */ 1415 try { 1416 return Integer.parseInt(value); 1417 } catch (NumberFormatException e) { 1418 1419 // Ignore as if it's not a number we don't care 1420 } 1421 } 1422 return mMinValue; 1423 } 1424 1425 /** 1426 * Posts an {@link SetSelectionCommand} from the given <code>selectionStart 1427 * </code> to 1428 * <code>selectionEnd</code>. 1429 */ 1430 private void postSetSelectionCommand(int selectionStart, int selectionEnd) { 1431 if (mSetSelectionCommand == null) { 1432 mSetSelectionCommand = new SetSelectionCommand(); 1433 } else { 1434 removeCallbacks(mSetSelectionCommand); 1435 } 1436 mSetSelectionCommand.mSelectionStart = selectionStart; 1437 mSetSelectionCommand.mSelectionEnd = selectionEnd; 1438 post(mSetSelectionCommand); 1439 } 1440 1441 /** 1442 * Posts an {@link AdjustScrollerCommand} within the given <code> 1443 * delayMillis</code> 1444 * . 1445 */ 1446 private void postAdjustScrollerCommand(int delayMillis) { 1447 if (mAdjustScrollerCommand == null) { 1448 mAdjustScrollerCommand = new AdjustScrollerCommand(); 1449 } else { 1450 removeCallbacks(mAdjustScrollerCommand); 1451 } 1452 postDelayed(mAdjustScrollerCommand, delayMillis); 1453 } 1454 1455 /** 1456 * Filter for accepting only valid indices or prefixes of the string 1457 * representation of valid indices. 1458 */ 1459 class InputTextFilter extends NumberKeyListener { 1460 1461 // XXX This doesn't allow for range limits when controlled by a 1462 // soft input method! 1463 public int getInputType() { 1464 return InputType.TYPE_CLASS_TEXT; 1465 } 1466 1467 @Override 1468 protected char[] getAcceptedChars() { 1469 return DIGIT_CHARACTERS; 1470 } 1471 1472 @Override 1473 public CharSequence filter(CharSequence source, int start, int end, Spanned dest, 1474 int dstart, int dend) { 1475 if (mDisplayedValues == null) { 1476 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); 1477 if (filtered == null) { 1478 filtered = source.subSequence(start, end); 1479 } 1480 1481 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1482 + dest.subSequence(dend, dest.length()); 1483 1484 if ("".equals(result)) { 1485 return result; 1486 } 1487 int val = getSelectedPos(result); 1488 1489 /* 1490 * Ensure the user can't type in a value greater than the max 1491 * allowed. We have to allow less than min as the user might 1492 * want to delete some numbers and then type a new number. 1493 */ 1494 if (val > mMaxValue) { 1495 return ""; 1496 } else { 1497 return filtered; 1498 } 1499 } else { 1500 CharSequence filtered = String.valueOf(source.subSequence(start, end)); 1501 if (TextUtils.isEmpty(filtered)) { 1502 return ""; 1503 } 1504 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1505 + dest.subSequence(dend, dest.length()); 1506 String str = String.valueOf(result).toLowerCase(); 1507 for (String val : mDisplayedValues) { 1508 String valLowerCase = val.toLowerCase(); 1509 if (valLowerCase.startsWith(str)) { 1510 postSetSelectionCommand(result.length(), val.length()); 1511 return val.subSequence(dstart, val.length()); 1512 } 1513 } 1514 return ""; 1515 } 1516 } 1517 } 1518 1519 /** 1520 * Command for setting the input text selection. 1521 */ 1522 class SetSelectionCommand implements Runnable { 1523 private int mSelectionStart; 1524 1525 private int mSelectionEnd; 1526 1527 public void run() { 1528 mInputText.setSelection(mSelectionStart, mSelectionEnd); 1529 } 1530 } 1531 1532 /** 1533 * Command for adjusting the scroller to show in its center the closest of 1534 * the displayed items. 1535 */ 1536 class AdjustScrollerCommand implements Runnable { 1537 public void run() { 1538 mPreviousScrollerY = 0; 1539 if (mInitialScrollOffset == mCurrentScrollOffset) { 1540 showInputControls(); 1541 updateInputTextView(); 1542 return; 1543 } 1544 // adjust to the closest value 1545 int deltaY = mInitialScrollOffset - mCurrentScrollOffset; 1546 if (Math.abs(deltaY) > mSelectorElementHeight / 2) { 1547 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; 1548 } 1549 float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize; 1550 int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS); 1551 mAdjustScroller.startScroll(0, 0, 0, deltaY, duration); 1552 invalidate(); 1553 } 1554 } 1555 1556 /** 1557 * Command for updating the current value from a long press. 1558 */ 1559 class UpdateValueFromLongPressCommand implements Runnable { 1560 private int mUpdateStep = 0; 1561 1562 private void setUpdateStep(int updateStep) { 1563 mUpdateStep = updateStep; 1564 } 1565 1566 public void run() { 1567 changeCurrent(mValue + mUpdateStep); 1568 postDelayed(this, mLongPressUpdateInterval); 1569 } 1570 } 1571} 1572