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