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