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