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