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