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