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