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