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