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