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