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