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