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