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