NumberPicker.java revision d88e3054d08b1f821bb2e27b2647aa16a60d8742
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.annotation.Widget; 20import android.content.Context; 21import android.content.res.ColorStateList; 22import android.content.res.TypedArray; 23import android.graphics.Canvas; 24import android.graphics.Color; 25import android.graphics.Paint; 26import android.graphics.Paint.Align; 27import android.graphics.Rect; 28import android.graphics.drawable.Drawable; 29import android.os.Bundle; 30import android.text.InputFilter; 31import android.text.InputType; 32import android.text.Spanned; 33import android.text.TextUtils; 34import android.text.method.NumberKeyListener; 35import android.util.AttributeSet; 36import android.util.SparseArray; 37import android.util.TypedValue; 38import android.view.KeyEvent; 39import android.view.LayoutInflater; 40import android.view.LayoutInflater.Filter; 41import android.view.MotionEvent; 42import android.view.VelocityTracker; 43import android.view.View; 44import android.view.ViewConfiguration; 45import android.view.accessibility.AccessibilityEvent; 46import android.view.accessibility.AccessibilityManager; 47import android.view.accessibility.AccessibilityNodeInfo; 48import android.view.accessibility.AccessibilityNodeProvider; 49import android.view.animation.DecelerateInterpolator; 50import android.view.inputmethod.EditorInfo; 51import android.view.inputmethod.InputMethodManager; 52 53import com.android.internal.R; 54import libcore.icu.LocaleData; 55 56import java.util.ArrayList; 57import java.util.Collections; 58import java.util.List; 59import java.util.Locale; 60 61/** 62 * A widget that enables the user to select a number form a predefined range. 63 * There are two flavors of this widget and which one is presented to the user 64 * depends on the current theme. 65 * <ul> 66 * <li> 67 * If the current theme is derived from {@link android.R.style#Theme} the widget 68 * presents the current value as an editable input field with an increment button 69 * above and a decrement button below. Long pressing the buttons allows for a quick 70 * change of the current value. Tapping on the input field allows to type in 71 * a desired value. 72 * </li> 73 * <li> 74 * If the current theme is derived from {@link android.R.style#Theme_Holo} or 75 * {@link android.R.style#Theme_Holo_Light} the widget presents the current 76 * value as an editable input field with a lesser value above and a greater 77 * value below. Tapping on the lesser or greater value selects it by animating 78 * the number axis up or down to make the chosen value current. Flinging up 79 * or down allows for multiple increments or decrements of the current value. 80 * Long pressing on the lesser and greater values also allows for a quick change 81 * of the current value. Tapping on the current value allows to type in a 82 * desired value. 83 * </li> 84 * </ul> 85 * <p> 86 * For an example of using this widget, see {@link android.widget.TimePicker}. 87 * </p> 88 */ 89@Widget 90public class NumberPicker extends LinearLayout { 91 92 /** 93 * The number of items show in the selector wheel. 94 */ 95 private static final int SELECTOR_WHEEL_ITEM_COUNT = 3; 96 97 /** 98 * The default update interval during long press. 99 */ 100 private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; 101 102 /** 103 * The index of the middle selector item. 104 */ 105 private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2; 106 107 /** 108 * The coefficient by which to adjust (divide) the max fling velocity. 109 */ 110 private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; 111 112 /** 113 * The the duration for adjusting the selector wheel. 114 */ 115 private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; 116 117 /** 118 * The duration of scrolling while snapping to a given position. 119 */ 120 private static final int SNAP_SCROLL_DURATION = 300; 121 122 /** 123 * The strength of fading in the top and bottom while drawing the selector. 124 */ 125 private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; 126 127 /** 128 * The default unscaled height of the selection divider. 129 */ 130 private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2; 131 132 /** 133 * The default unscaled distance between the selection dividers. 134 */ 135 private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48; 136 137 /** 138 * The resource id for the default layout. 139 */ 140 private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker; 141 142 /** 143 * Constant for unspecified size. 144 */ 145 private static final int SIZE_UNSPECIFIED = -1; 146 147 /** 148 * Use a custom NumberPicker formatting callback to use two-digit minutes 149 * strings like "01". Keeping a static formatter etc. is the most efficient 150 * way to do this; it avoids creating temporary objects on every call to 151 * format(). 152 * 153 */ 154 private static class TwoDigitFormatter implements NumberPicker.Formatter { 155 final StringBuilder mBuilder = new StringBuilder(); 156 157 char mZeroDigit; 158 java.util.Formatter mFmt; 159 160 final Object[] mArgs = new Object[1]; 161 162 TwoDigitFormatter() { 163 final Locale locale = Locale.getDefault(); 164 init(locale); 165 } 166 167 private void init(Locale locale) { 168 mFmt = createFormatter(locale); 169 mZeroDigit = getZeroDigit(locale); 170 } 171 172 public String format(int value) { 173 final Locale currentLocale = Locale.getDefault(); 174 if (mZeroDigit != getZeroDigit(currentLocale)) { 175 init(currentLocale); 176 } 177 mArgs[0] = value; 178 mBuilder.delete(0, mBuilder.length()); 179 mFmt.format("%02d", mArgs); 180 return mFmt.toString(); 181 } 182 183 private static char getZeroDigit(Locale locale) { 184 return LocaleData.get(locale).zeroDigit; 185 } 186 187 private java.util.Formatter createFormatter(Locale locale) { 188 return new java.util.Formatter(mBuilder, locale); 189 } 190 } 191 192 private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter(); 193 194 static final Formatter getTwoDigitFormatter() { 195 return sTwoDigitFormatter; 196 } 197 198 199 /** 200 * The increment button. 201 */ 202 private final ImageButton mIncrementButton; 203 204 /** 205 * The decrement button. 206 */ 207 private final ImageButton mDecrementButton; 208 209 /** 210 * The text for showing the current value. 211 */ 212 private final EditText mInputText; 213 214 /** 215 * The distance between the two selection dividers. 216 */ 217 private final int mSelectionDividersDistance; 218 219 /** 220 * The min height of this widget. 221 */ 222 private final int mMinHeight; 223 224 /** 225 * The max height of this widget. 226 */ 227 private final int mMaxHeight; 228 229 /** 230 * The max width of this widget. 231 */ 232 private final int mMinWidth; 233 234 /** 235 * The max width of this widget. 236 */ 237 private int mMaxWidth; 238 239 /** 240 * Flag whether to compute the max width. 241 */ 242 private final boolean mComputeMaxWidth; 243 244 /** 245 * The height of the text. 246 */ 247 private final int mTextSize; 248 249 /** 250 * The height of the gap between text elements if the selector wheel. 251 */ 252 private int mSelectorTextGapHeight; 253 254 /** 255 * The values to be displayed instead the indices. 256 */ 257 private String[] mDisplayedValues; 258 259 /** 260 * Lower value of the range of numbers allowed for the NumberPicker 261 */ 262 private int mMinValue; 263 264 /** 265 * Upper value of the range of numbers allowed for the NumberPicker 266 */ 267 private int mMaxValue; 268 269 /** 270 * Current value of this NumberPicker 271 */ 272 private int mValue; 273 274 /** 275 * Listener to be notified upon current value change. 276 */ 277 private OnValueChangeListener mOnValueChangeListener; 278 279 /** 280 * Listener to be notified upon scroll state change. 281 */ 282 private OnScrollListener mOnScrollListener; 283 284 /** 285 * Formatter for for displaying the current value. 286 */ 287 private Formatter mFormatter; 288 289 /** 290 * The speed for updating the value form long press. 291 */ 292 private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; 293 294 /** 295 * Cache for the string representation of selector indices. 296 */ 297 private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>(); 298 299 /** 300 * The selector indices whose value are show by the selector. 301 */ 302 private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT]; 303 304 /** 305 * The {@link Paint} for drawing the selector. 306 */ 307 private final Paint mSelectorWheelPaint; 308 309 /** 310 * The {@link Drawable} for pressed virtual (increment/decrement) buttons. 311 */ 312 private final Drawable mVirtualButtonPressedDrawable; 313 314 /** 315 * The height of a selector element (text + gap). 316 */ 317 private int mSelectorElementHeight; 318 319 /** 320 * The initial offset of the scroll selector. 321 */ 322 private int mInitialScrollOffset = Integer.MIN_VALUE; 323 324 /** 325 * The current offset of the scroll selector. 326 */ 327 private int mCurrentScrollOffset; 328 329 /** 330 * The {@link Scroller} responsible for flinging the selector. 331 */ 332 private final Scroller mFlingScroller; 333 334 /** 335 * The {@link Scroller} responsible for adjusting the selector. 336 */ 337 private final Scroller mAdjustScroller; 338 339 /** 340 * The previous Y coordinate while scrolling the selector. 341 */ 342 private int mPreviousScrollerY; 343 344 /** 345 * Handle to the reusable command for setting the input text selection. 346 */ 347 private SetSelectionCommand mSetSelectionCommand; 348 349 /** 350 * Handle to the reusable command for changing the current value from long 351 * press by one. 352 */ 353 private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand; 354 355 /** 356 * Command for beginning an edit of the current value via IME on long press. 357 */ 358 private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand; 359 360 /** 361 * The Y position of the last down event. 362 */ 363 private float mLastDownEventY; 364 365 /** 366 * The time of the last down event. 367 */ 368 private long mLastDownEventTime; 369 370 /** 371 * The Y position of the last down or move event. 372 */ 373 private float mLastDownOrMoveEventY; 374 375 /** 376 * Determines speed during touch scrolling. 377 */ 378 private VelocityTracker mVelocityTracker; 379 380 /** 381 * @see ViewConfiguration#getScaledTouchSlop() 382 */ 383 private int mTouchSlop; 384 385 /** 386 * @see ViewConfiguration#getScaledMinimumFlingVelocity() 387 */ 388 private int mMinimumFlingVelocity; 389 390 /** 391 * @see ViewConfiguration#getScaledMaximumFlingVelocity() 392 */ 393 private int mMaximumFlingVelocity; 394 395 /** 396 * Flag whether the selector should wrap around. 397 */ 398 private boolean mWrapSelectorWheel; 399 400 /** 401 * The back ground color used to optimize scroller fading. 402 */ 403 private final int mSolidColor; 404 405 /** 406 * Flag whether this widget has a selector wheel. 407 */ 408 private final boolean mHasSelectorWheel; 409 410 /** 411 * Divider for showing item to be selected while scrolling 412 */ 413 private final Drawable mSelectionDivider; 414 415 /** 416 * The height of the selection divider. 417 */ 418 private final int mSelectionDividerHeight; 419 420 /** 421 * The current scroll state of the number picker. 422 */ 423 private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; 424 425 /** 426 * Flag whether to ignore move events - we ignore such when we show in IME 427 * to prevent the content from scrolling. 428 */ 429 private boolean mIngonreMoveEvents; 430 431 /** 432 * Flag whether to show soft input on tap. 433 */ 434 private boolean mShowSoftInputOnTap; 435 436 /** 437 * The top of the top selection divider. 438 */ 439 private int mTopSelectionDividerTop; 440 441 /** 442 * The bottom of the bottom selection divider. 443 */ 444 private int mBottomSelectionDividerBottom; 445 446 /** 447 * The virtual id of the last hovered child. 448 */ 449 private int mLastHoveredChildVirtualViewId; 450 451 /** 452 * Whether the increment virtual button is pressed. 453 */ 454 private boolean mIncrementVirtualButtonPressed; 455 456 /** 457 * Whether the decrement virtual button is pressed. 458 */ 459 private boolean mDecrementVirtualButtonPressed; 460 461 /** 462 * Provider to report to clients the semantic structure of this widget. 463 */ 464 private AccessibilityNodeProviderImpl mAccessibilityNodeProvider; 465 466 /** 467 * Helper class for managing pressed state of the virtual buttons. 468 */ 469 private final PressedStateHelper mPressedStateHelper; 470 471 /** 472 * Interface to listen for changes of the current value. 473 */ 474 public interface OnValueChangeListener { 475 476 /** 477 * Called upon a change of the current value. 478 * 479 * @param picker The NumberPicker associated with this listener. 480 * @param oldVal The previous value. 481 * @param newVal The new value. 482 */ 483 void onValueChange(NumberPicker picker, int oldVal, int newVal); 484 } 485 486 /** 487 * Interface to listen for the picker scroll state. 488 */ 489 public interface OnScrollListener { 490 491 /** 492 * The view is not scrolling. 493 */ 494 public static int SCROLL_STATE_IDLE = 0; 495 496 /** 497 * The user is scrolling using touch, and his finger is still on the screen. 498 */ 499 public static int SCROLL_STATE_TOUCH_SCROLL = 1; 500 501 /** 502 * The user had previously been scrolling using touch and performed a fling. 503 */ 504 public static int SCROLL_STATE_FLING = 2; 505 506 /** 507 * Callback invoked while the number picker scroll state has changed. 508 * 509 * @param view The view whose scroll state is being reported. 510 * @param scrollState The current scroll state. One of 511 * {@link #SCROLL_STATE_IDLE}, 512 * {@link #SCROLL_STATE_TOUCH_SCROLL} or 513 * {@link #SCROLL_STATE_IDLE}. 514 */ 515 public void onScrollStateChange(NumberPicker view, int scrollState); 516 } 517 518 /** 519 * Interface used to format current value into a string for presentation. 520 */ 521 public interface Formatter { 522 523 /** 524 * Formats a string representation of the current value. 525 * 526 * @param value The currently selected value. 527 * @return A formatted string representation. 528 */ 529 public String format(int value); 530 } 531 532 /** 533 * Create a new number picker. 534 * 535 * @param context The application environment. 536 */ 537 public NumberPicker(Context context) { 538 this(context, null); 539 } 540 541 /** 542 * Create a new number picker. 543 * 544 * @param context The application environment. 545 * @param attrs A collection of attributes. 546 */ 547 public NumberPicker(Context context, AttributeSet attrs) { 548 this(context, attrs, R.attr.numberPickerStyle); 549 } 550 551 /** 552 * Create a new number picker 553 * 554 * @param context the application environment. 555 * @param attrs a collection of attributes. 556 * @param defStyle The default style to apply to this view. 557 */ 558 public NumberPicker(Context context, AttributeSet attrs, int defStyle) { 559 super(context, attrs, defStyle); 560 561 // process style attributes 562 TypedArray attributesArray = context.obtainStyledAttributes( 563 attrs, R.styleable.NumberPicker, defStyle, 0); 564 final int layoutResId = attributesArray.getResourceId( 565 R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID); 566 567 mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID); 568 569 mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); 570 571 mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider); 572 573 final int defSelectionDividerHeight = (int) TypedValue.applyDimension( 574 TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, 575 getResources().getDisplayMetrics()); 576 mSelectionDividerHeight = attributesArray.getDimensionPixelSize( 577 R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight); 578 579 final int defSelectionDividerDistance = (int) TypedValue.applyDimension( 580 TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE, 581 getResources().getDisplayMetrics()); 582 mSelectionDividersDistance = attributesArray.getDimensionPixelSize( 583 R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance); 584 585 mMinHeight = attributesArray.getDimensionPixelSize( 586 R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED); 587 588 mMaxHeight = attributesArray.getDimensionPixelSize( 589 R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED); 590 if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED 591 && mMinHeight > mMaxHeight) { 592 throw new IllegalArgumentException("minHeight > maxHeight"); 593 } 594 595 mMinWidth = attributesArray.getDimensionPixelSize( 596 R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED); 597 598 mMaxWidth = attributesArray.getDimensionPixelSize( 599 R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED); 600 if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED 601 && mMinWidth > mMaxWidth) { 602 throw new IllegalArgumentException("minWidth > maxWidth"); 603 } 604 605 mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED); 606 607 mVirtualButtonPressedDrawable = attributesArray.getDrawable( 608 R.styleable.NumberPicker_virtualButtonPressedDrawable); 609 610 attributesArray.recycle(); 611 612 mPressedStateHelper = new PressedStateHelper(); 613 614 // By default Linearlayout that we extend is not drawn. This is 615 // its draw() method is not called but dispatchDraw() is called 616 // directly (see ViewGroup.drawChild()). However, this class uses 617 // the fading edge effect implemented by View and we need our 618 // draw() method to be called. Therefore, we declare we will draw. 619 setWillNotDraw(!mHasSelectorWheel); 620 621 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 622 Context.LAYOUT_INFLATER_SERVICE); 623 inflater.inflate(layoutResId, this, true); 624 625 OnClickListener onClickListener = new OnClickListener() { 626 public void onClick(View v) { 627 hideSoftInput(); 628 mInputText.clearFocus(); 629 if (v.getId() == R.id.increment) { 630 changeValueByOne(true); 631 } else { 632 changeValueByOne(false); 633 } 634 } 635 }; 636 637 OnLongClickListener onLongClickListener = new OnLongClickListener() { 638 public boolean onLongClick(View v) { 639 hideSoftInput(); 640 mInputText.clearFocus(); 641 if (v.getId() == R.id.increment) { 642 postChangeCurrentByOneFromLongPress(true, 0); 643 } else { 644 postChangeCurrentByOneFromLongPress(false, 0); 645 } 646 return true; 647 } 648 }; 649 650 // increment button 651 if (!mHasSelectorWheel) { 652 mIncrementButton = (ImageButton) findViewById(R.id.increment); 653 mIncrementButton.setOnClickListener(onClickListener); 654 mIncrementButton.setOnLongClickListener(onLongClickListener); 655 } else { 656 mIncrementButton = null; 657 } 658 659 // decrement button 660 if (!mHasSelectorWheel) { 661 mDecrementButton = (ImageButton) findViewById(R.id.decrement); 662 mDecrementButton.setOnClickListener(onClickListener); 663 mDecrementButton.setOnLongClickListener(onLongClickListener); 664 } else { 665 mDecrementButton = null; 666 } 667 668 // input text 669 mInputText = (EditText) findViewById(R.id.numberpicker_input); 670 mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { 671 public void onFocusChange(View v, boolean hasFocus) { 672 if (hasFocus) { 673 mInputText.selectAll(); 674 } else { 675 mInputText.setSelection(0, 0); 676 validateInputTextView(v); 677 } 678 } 679 }); 680 mInputText.setFilters(new InputFilter[] { 681 new InputTextFilter() 682 }); 683 684 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 685 mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE); 686 687 // initialize constants 688 ViewConfiguration configuration = ViewConfiguration.get(context); 689 mTouchSlop = configuration.getScaledTouchSlop(); 690 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 691 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() 692 / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; 693 mTextSize = (int) mInputText.getTextSize(); 694 695 // create the selector wheel paint 696 Paint paint = new Paint(); 697 paint.setAntiAlias(true); 698 paint.setTextAlign(Align.CENTER); 699 paint.setTextSize(mTextSize); 700 paint.setTypeface(mInputText.getTypeface()); 701 ColorStateList colors = mInputText.getTextColors(); 702 int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); 703 paint.setColor(color); 704 mSelectorWheelPaint = paint; 705 706 // create the fling and adjust scrollers 707 mFlingScroller = new Scroller(getContext(), null, true); 708 mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); 709 710 updateInputTextView(); 711 712 // If not explicitly specified this view is important for accessibility. 713 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 714 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 715 } 716 } 717 718 @Override 719 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 720 if (!mHasSelectorWheel) { 721 super.onLayout(changed, left, top, right, bottom); 722 return; 723 } 724 final int msrdWdth = getMeasuredWidth(); 725 final int msrdHght = getMeasuredHeight(); 726 727 // Input text centered horizontally. 728 final int inptTxtMsrdWdth = mInputText.getMeasuredWidth(); 729 final int inptTxtMsrdHght = mInputText.getMeasuredHeight(); 730 final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2; 731 final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2; 732 final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth; 733 final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; 734 mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); 735 736 if (changed) { 737 // need to do all this when we know our size 738 initializeSelectorWheel(); 739 initializeFadingEdges(); 740 mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2 741 - mSelectionDividerHeight; 742 mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight 743 + mSelectionDividersDistance; 744 } 745 } 746 747 @Override 748 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 749 if (!mHasSelectorWheel) { 750 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 751 return; 752 } 753 // Try greedily to fit the max width and height. 754 final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); 755 final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); 756 super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); 757 // Flag if we are measured with width or height less than the respective min. 758 final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), 759 widthMeasureSpec); 760 final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), 761 heightMeasureSpec); 762 setMeasuredDimension(widthSize, heightSize); 763 } 764 765 /** 766 * Move to the final position of a scroller. Ensures to force finish the scroller 767 * and if it is not at its final position a scroll of the selector wheel is 768 * performed to fast forward to the final position. 769 * 770 * @param scroller The scroller to whose final position to get. 771 * @return True of the a move was performed, i.e. the scroller was not in final position. 772 */ 773 private boolean moveToFinalScrollerPosition(Scroller scroller) { 774 scroller.forceFinished(true); 775 int amountToScroll = scroller.getFinalY() - scroller.getCurrY(); 776 int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight; 777 int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; 778 if (overshootAdjustment != 0) { 779 if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) { 780 if (overshootAdjustment > 0) { 781 overshootAdjustment -= mSelectorElementHeight; 782 } else { 783 overshootAdjustment += mSelectorElementHeight; 784 } 785 } 786 amountToScroll += overshootAdjustment; 787 scrollBy(0, amountToScroll); 788 return true; 789 } 790 return false; 791 } 792 793 @Override 794 public boolean onInterceptTouchEvent(MotionEvent event) { 795 if (!mHasSelectorWheel || !isEnabled()) { 796 return false; 797 } 798 final int action = event.getActionMasked(); 799 switch (action) { 800 case MotionEvent.ACTION_DOWN: { 801 removeAllCallbacks(); 802 mInputText.setVisibility(View.INVISIBLE); 803 mLastDownOrMoveEventY = mLastDownEventY = event.getY(); 804 mLastDownEventTime = event.getEventTime(); 805 mIngonreMoveEvents = false; 806 mShowSoftInputOnTap = false; 807 // Handle pressed state before any state change. 808 if (mLastDownEventY < mTopSelectionDividerTop) { 809 if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { 810 mPressedStateHelper.buttonPressDelayed( 811 PressedStateHelper.BUTTON_DECREMENT); 812 } 813 } else if (mLastDownEventY > mBottomSelectionDividerBottom) { 814 if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { 815 mPressedStateHelper.buttonPressDelayed( 816 PressedStateHelper.BUTTON_INCREMENT); 817 } 818 } 819 // Make sure we support flinging inside scrollables. 820 getParent().requestDisallowInterceptTouchEvent(true); 821 if (!mFlingScroller.isFinished()) { 822 mFlingScroller.forceFinished(true); 823 mAdjustScroller.forceFinished(true); 824 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 825 } else if (!mAdjustScroller.isFinished()) { 826 mFlingScroller.forceFinished(true); 827 mAdjustScroller.forceFinished(true); 828 } else if (mLastDownEventY < mTopSelectionDividerTop) { 829 hideSoftInput(); 830 postChangeCurrentByOneFromLongPress( 831 false, ViewConfiguration.getLongPressTimeout()); 832 } else if (mLastDownEventY > mBottomSelectionDividerBottom) { 833 hideSoftInput(); 834 postChangeCurrentByOneFromLongPress( 835 true, ViewConfiguration.getLongPressTimeout()); 836 } else { 837 mShowSoftInputOnTap = true; 838 postBeginSoftInputOnLongPressCommand(); 839 } 840 return true; 841 } 842 } 843 return false; 844 } 845 846 @Override 847 public boolean onTouchEvent(MotionEvent event) { 848 if (!isEnabled() || !mHasSelectorWheel) { 849 return false; 850 } 851 if (mVelocityTracker == null) { 852 mVelocityTracker = VelocityTracker.obtain(); 853 } 854 mVelocityTracker.addMovement(event); 855 int action = event.getActionMasked(); 856 switch (action) { 857 case MotionEvent.ACTION_MOVE: { 858 if (mIngonreMoveEvents) { 859 break; 860 } 861 float currentMoveY = event.getY(); 862 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 863 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); 864 if (deltaDownY > mTouchSlop) { 865 removeAllCallbacks(); 866 onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 867 } 868 } else { 869 int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY)); 870 scrollBy(0, deltaMoveY); 871 invalidate(); 872 } 873 mLastDownOrMoveEventY = currentMoveY; 874 } break; 875 case MotionEvent.ACTION_UP: { 876 removeBeginSoftInputCommand(); 877 removeChangeCurrentByOneFromLongPress(); 878 mPressedStateHelper.cancel(); 879 VelocityTracker velocityTracker = mVelocityTracker; 880 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 881 int initialVelocity = (int) velocityTracker.getYVelocity(); 882 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { 883 fling(initialVelocity); 884 onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 885 } else { 886 int eventY = (int) event.getY(); 887 int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY); 888 long deltaTime = event.getEventTime() - mLastDownEventTime; 889 if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) { 890 if (mShowSoftInputOnTap) { 891 mShowSoftInputOnTap = false; 892 showSoftInput(); 893 } else { 894 int selectorIndexOffset = (eventY / mSelectorElementHeight) 895 - SELECTOR_MIDDLE_ITEM_INDEX; 896 if (selectorIndexOffset > 0) { 897 changeValueByOne(true); 898 mPressedStateHelper.buttonTapped( 899 PressedStateHelper.BUTTON_INCREMENT); 900 } else if (selectorIndexOffset < 0) { 901 changeValueByOne(false); 902 mPressedStateHelper.buttonTapped( 903 PressedStateHelper.BUTTON_DECREMENT); 904 } 905 } 906 } else { 907 ensureScrollWheelAdjusted(); 908 } 909 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 910 } 911 mVelocityTracker.recycle(); 912 mVelocityTracker = null; 913 } break; 914 } 915 return true; 916 } 917 918 @Override 919 public boolean dispatchTouchEvent(MotionEvent event) { 920 final int action = event.getActionMasked(); 921 switch (action) { 922 case MotionEvent.ACTION_CANCEL: 923 case MotionEvent.ACTION_UP: 924 removeAllCallbacks(); 925 break; 926 } 927 return super.dispatchTouchEvent(event); 928 } 929 930 @Override 931 public boolean dispatchKeyEvent(KeyEvent event) { 932 final int keyCode = event.getKeyCode(); 933 switch (keyCode) { 934 case KeyEvent.KEYCODE_DPAD_CENTER: 935 case KeyEvent.KEYCODE_ENTER: 936 removeAllCallbacks(); 937 break; 938 } 939 return super.dispatchKeyEvent(event); 940 } 941 942 @Override 943 public boolean dispatchTrackballEvent(MotionEvent event) { 944 final int action = event.getActionMasked(); 945 switch (action) { 946 case MotionEvent.ACTION_CANCEL: 947 case MotionEvent.ACTION_UP: 948 removeAllCallbacks(); 949 break; 950 } 951 return super.dispatchTrackballEvent(event); 952 } 953 954 @Override 955 protected boolean dispatchHoverEvent(MotionEvent event) { 956 if (!mHasSelectorWheel) { 957 return super.dispatchHoverEvent(event); 958 } 959 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 960 final int eventY = (int) event.getY(); 961 final int hoveredVirtualViewId; 962 if (eventY < mTopSelectionDividerTop) { 963 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT; 964 } else if (eventY > mBottomSelectionDividerBottom) { 965 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT; 966 } else { 967 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT; 968 } 969 final int action = event.getActionMasked(); 970 AccessibilityNodeProviderImpl provider = 971 (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider(); 972 switch (action) { 973 case MotionEvent.ACTION_HOVER_ENTER: { 974 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, 975 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 976 mLastHoveredChildVirtualViewId = hoveredVirtualViewId; 977 provider.performAction(hoveredVirtualViewId, 978 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 979 } break; 980 case MotionEvent.ACTION_HOVER_MOVE: { 981 if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId 982 && mLastHoveredChildVirtualViewId != View.NO_ID) { 983 provider.sendAccessibilityEventForVirtualView( 984 mLastHoveredChildVirtualViewId, 985 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 986 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, 987 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 988 mLastHoveredChildVirtualViewId = hoveredVirtualViewId; 989 provider.performAction(hoveredVirtualViewId, 990 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 991 } 992 } break; 993 case MotionEvent.ACTION_HOVER_EXIT: { 994 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, 995 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 996 mLastHoveredChildVirtualViewId = View.NO_ID; 997 } break; 998 } 999 } 1000 return false; 1001 } 1002 1003 @Override 1004 public void computeScroll() { 1005 Scroller scroller = mFlingScroller; 1006 if (scroller.isFinished()) { 1007 scroller = mAdjustScroller; 1008 if (scroller.isFinished()) { 1009 return; 1010 } 1011 } 1012 scroller.computeScrollOffset(); 1013 int currentScrollerY = scroller.getCurrY(); 1014 if (mPreviousScrollerY == 0) { 1015 mPreviousScrollerY = scroller.getStartY(); 1016 } 1017 scrollBy(0, currentScrollerY - mPreviousScrollerY); 1018 mPreviousScrollerY = currentScrollerY; 1019 if (scroller.isFinished()) { 1020 onScrollerFinished(scroller); 1021 } else { 1022 invalidate(); 1023 } 1024 } 1025 1026 @Override 1027 public void setEnabled(boolean enabled) { 1028 super.setEnabled(enabled); 1029 if (!mHasSelectorWheel) { 1030 mIncrementButton.setEnabled(enabled); 1031 } 1032 if (!mHasSelectorWheel) { 1033 mDecrementButton.setEnabled(enabled); 1034 } 1035 mInputText.setEnabled(enabled); 1036 } 1037 1038 @Override 1039 public void scrollBy(int x, int y) { 1040 int[] selectorIndices = mSelectorIndices; 1041 if (!mWrapSelectorWheel && y > 0 1042 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 1043 mCurrentScrollOffset = mInitialScrollOffset; 1044 return; 1045 } 1046 if (!mWrapSelectorWheel && y < 0 1047 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 1048 mCurrentScrollOffset = mInitialScrollOffset; 1049 return; 1050 } 1051 mCurrentScrollOffset += y; 1052 while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { 1053 mCurrentScrollOffset -= mSelectorElementHeight; 1054 decrementSelectorIndices(selectorIndices); 1055 setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); 1056 if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 1057 mCurrentScrollOffset = mInitialScrollOffset; 1058 } 1059 } 1060 while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { 1061 mCurrentScrollOffset += mSelectorElementHeight; 1062 incrementSelectorIndices(selectorIndices); 1063 setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); 1064 if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 1065 mCurrentScrollOffset = mInitialScrollOffset; 1066 } 1067 } 1068 } 1069 1070 @Override 1071 public int getSolidColor() { 1072 return mSolidColor; 1073 } 1074 1075 /** 1076 * Sets the listener to be notified on change of the current value. 1077 * 1078 * @param onValueChangedListener The listener. 1079 */ 1080 public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { 1081 mOnValueChangeListener = onValueChangedListener; 1082 } 1083 1084 /** 1085 * Set listener to be notified for scroll state changes. 1086 * 1087 * @param onScrollListener The listener. 1088 */ 1089 public void setOnScrollListener(OnScrollListener onScrollListener) { 1090 mOnScrollListener = onScrollListener; 1091 } 1092 1093 /** 1094 * Set the formatter to be used for formatting the current value. 1095 * <p> 1096 * Note: If you have provided alternative values for the values this 1097 * formatter is never invoked. 1098 * </p> 1099 * 1100 * @param formatter The formatter object. If formatter is <code>null</code>, 1101 * {@link String#valueOf(int)} will be used. 1102 *@see #setDisplayedValues(String[]) 1103 */ 1104 public void setFormatter(Formatter formatter) { 1105 if (formatter == mFormatter) { 1106 return; 1107 } 1108 mFormatter = formatter; 1109 initializeSelectorWheelIndices(); 1110 updateInputTextView(); 1111 } 1112 1113 /** 1114 * Set the current value for the number picker. 1115 * <p> 1116 * If the argument is less than the {@link NumberPicker#getMinValue()} and 1117 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 1118 * current value is set to the {@link NumberPicker#getMinValue()} value. 1119 * </p> 1120 * <p> 1121 * If the argument is less than the {@link NumberPicker#getMinValue()} and 1122 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 1123 * current value is set to the {@link NumberPicker#getMaxValue()} value. 1124 * </p> 1125 * <p> 1126 * If the argument is less than the {@link NumberPicker#getMaxValue()} and 1127 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 1128 * current value is set to the {@link NumberPicker#getMaxValue()} value. 1129 * </p> 1130 * <p> 1131 * If the argument is less than the {@link NumberPicker#getMaxValue()} and 1132 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 1133 * current value is set to the {@link NumberPicker#getMinValue()} value. 1134 * </p> 1135 * 1136 * @param value The current value. 1137 * @see #setWrapSelectorWheel(boolean) 1138 * @see #setMinValue(int) 1139 * @see #setMaxValue(int) 1140 */ 1141 public void setValue(int value) { 1142 setValueInternal(value, false); 1143 } 1144 1145 /** 1146 * Shows the soft input for its input text. 1147 */ 1148 private void showSoftInput() { 1149 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 1150 if (inputMethodManager != null) { 1151 if (mHasSelectorWheel) { 1152 mInputText.setVisibility(View.VISIBLE); 1153 } 1154 mInputText.requestFocus(); 1155 inputMethodManager.showSoftInput(mInputText, 0); 1156 } 1157 } 1158 1159 /** 1160 * Hides the soft input if it is active for the input text. 1161 */ 1162 private void hideSoftInput() { 1163 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 1164 if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { 1165 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 1166 if (mHasSelectorWheel) { 1167 mInputText.setVisibility(View.INVISIBLE); 1168 } 1169 } 1170 } 1171 1172 /** 1173 * Computes the max width if no such specified as an attribute. 1174 */ 1175 private void tryComputeMaxWidth() { 1176 if (!mComputeMaxWidth) { 1177 return; 1178 } 1179 int maxTextWidth = 0; 1180 if (mDisplayedValues == null) { 1181 float maxDigitWidth = 0; 1182 for (int i = 0; i <= 9; i++) { 1183 final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i)); 1184 if (digitWidth > maxDigitWidth) { 1185 maxDigitWidth = digitWidth; 1186 } 1187 } 1188 int numberOfDigits = 0; 1189 int current = mMaxValue; 1190 while (current > 0) { 1191 numberOfDigits++; 1192 current = current / 10; 1193 } 1194 maxTextWidth = (int) (numberOfDigits * maxDigitWidth); 1195 } else { 1196 final int valueCount = mDisplayedValues.length; 1197 for (int i = 0; i < valueCount; i++) { 1198 final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]); 1199 if (textWidth > maxTextWidth) { 1200 maxTextWidth = (int) textWidth; 1201 } 1202 } 1203 } 1204 maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight(); 1205 if (mMaxWidth != maxTextWidth) { 1206 if (maxTextWidth > mMinWidth) { 1207 mMaxWidth = maxTextWidth; 1208 } else { 1209 mMaxWidth = mMinWidth; 1210 } 1211 invalidate(); 1212 } 1213 } 1214 1215 /** 1216 * Gets whether the selector wheel wraps when reaching the min/max value. 1217 * 1218 * @return True if the selector wheel wraps. 1219 * 1220 * @see #getMinValue() 1221 * @see #getMaxValue() 1222 */ 1223 public boolean getWrapSelectorWheel() { 1224 return mWrapSelectorWheel; 1225 } 1226 1227 /** 1228 * Sets whether the selector wheel shown during flinging/scrolling should 1229 * wrap around the {@link NumberPicker#getMinValue()} and 1230 * {@link NumberPicker#getMaxValue()} values. 1231 * <p> 1232 * By default if the range (max - min) is more than the number of items shown 1233 * on the selector wheel the selector wheel wrapping is enabled. 1234 * </p> 1235 * <p> 1236 * <strong>Note:</strong> If the number of items, i.e. the range ( 1237 * {@link #getMaxValue()} - {@link #getMinValue()}) is less than 1238 * the number of items shown on the selector wheel, the selector wheel will 1239 * not wrap. Hence, in such a case calling this method is a NOP. 1240 * </p> 1241 * 1242 * @param wrapSelectorWheel Whether to wrap. 1243 */ 1244 public void setWrapSelectorWheel(boolean wrapSelectorWheel) { 1245 final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; 1246 if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) { 1247 mWrapSelectorWheel = wrapSelectorWheel; 1248 } 1249 } 1250 1251 /** 1252 * Sets the speed at which the numbers be incremented and decremented when 1253 * the up and down buttons are long pressed respectively. 1254 * <p> 1255 * The default value is 300 ms. 1256 * </p> 1257 * 1258 * @param intervalMillis The speed (in milliseconds) at which the numbers 1259 * will be incremented and decremented. 1260 */ 1261 public void setOnLongPressUpdateInterval(long intervalMillis) { 1262 mLongPressUpdateInterval = intervalMillis; 1263 } 1264 1265 /** 1266 * Returns the value of the picker. 1267 * 1268 * @return The value. 1269 */ 1270 public int getValue() { 1271 return mValue; 1272 } 1273 1274 /** 1275 * Returns the min value of the picker. 1276 * 1277 * @return The min value 1278 */ 1279 public int getMinValue() { 1280 return mMinValue; 1281 } 1282 1283 /** 1284 * Sets the min value of the picker. 1285 * 1286 * @param minValue The min value. 1287 */ 1288 public void setMinValue(int minValue) { 1289 if (mMinValue == minValue) { 1290 return; 1291 } 1292 if (minValue < 0) { 1293 throw new IllegalArgumentException("minValue must be >= 0"); 1294 } 1295 mMinValue = minValue; 1296 if (mMinValue > mValue) { 1297 mValue = mMinValue; 1298 } 1299 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; 1300 setWrapSelectorWheel(wrapSelectorWheel); 1301 initializeSelectorWheelIndices(); 1302 updateInputTextView(); 1303 tryComputeMaxWidth(); 1304 invalidate(); 1305 } 1306 1307 /** 1308 * Returns the max value of the picker. 1309 * 1310 * @return The max value. 1311 */ 1312 public int getMaxValue() { 1313 return mMaxValue; 1314 } 1315 1316 /** 1317 * Sets the max value of the picker. 1318 * 1319 * @param maxValue The max value. 1320 */ 1321 public void setMaxValue(int maxValue) { 1322 if (mMaxValue == maxValue) { 1323 return; 1324 } 1325 if (maxValue < 0) { 1326 throw new IllegalArgumentException("maxValue must be >= 0"); 1327 } 1328 mMaxValue = maxValue; 1329 if (mMaxValue < mValue) { 1330 mValue = mMaxValue; 1331 } 1332 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; 1333 setWrapSelectorWheel(wrapSelectorWheel); 1334 initializeSelectorWheelIndices(); 1335 updateInputTextView(); 1336 tryComputeMaxWidth(); 1337 invalidate(); 1338 } 1339 1340 /** 1341 * Gets the values to be displayed instead of string values. 1342 * 1343 * @return The displayed values. 1344 */ 1345 public String[] getDisplayedValues() { 1346 return mDisplayedValues; 1347 } 1348 1349 /** 1350 * Sets the values to be displayed. 1351 * 1352 * @param displayedValues The displayed values. 1353 */ 1354 public void setDisplayedValues(String[] displayedValues) { 1355 if (mDisplayedValues == displayedValues) { 1356 return; 1357 } 1358 mDisplayedValues = displayedValues; 1359 if (mDisplayedValues != null) { 1360 // Allow text entry rather than strictly numeric entry. 1361 mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT 1362 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 1363 } else { 1364 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 1365 } 1366 updateInputTextView(); 1367 initializeSelectorWheelIndices(); 1368 tryComputeMaxWidth(); 1369 } 1370 1371 @Override 1372 protected float getTopFadingEdgeStrength() { 1373 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1374 } 1375 1376 @Override 1377 protected float getBottomFadingEdgeStrength() { 1378 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1379 } 1380 1381 @Override 1382 protected void onDetachedFromWindow() { 1383 removeAllCallbacks(); 1384 } 1385 1386 @Override 1387 protected void onDraw(Canvas canvas) { 1388 if (!mHasSelectorWheel) { 1389 super.onDraw(canvas); 1390 return; 1391 } 1392 float x = (mRight - mLeft) / 2; 1393 float y = mCurrentScrollOffset; 1394 1395 // draw the virtual buttons pressed state if needed 1396 if (mVirtualButtonPressedDrawable != null 1397 && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { 1398 if (mDecrementVirtualButtonPressed) { 1399 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); 1400 mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop); 1401 mVirtualButtonPressedDrawable.draw(canvas); 1402 } 1403 if (mIncrementVirtualButtonPressed) { 1404 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); 1405 mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight, 1406 mBottom); 1407 mVirtualButtonPressedDrawable.draw(canvas); 1408 } 1409 } 1410 1411 // draw the selector wheel 1412 int[] selectorIndices = mSelectorIndices; 1413 for (int i = 0; i < selectorIndices.length; i++) { 1414 int selectorIndex = selectorIndices[i]; 1415 String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); 1416 // Do not draw the middle item if input is visible since the input 1417 // is shown only if the wheel is static and it covers the middle 1418 // item. Otherwise, if the user starts editing the text via the 1419 // IME he may see a dimmed version of the old value intermixed 1420 // with the new one. 1421 if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) { 1422 canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); 1423 } 1424 y += mSelectorElementHeight; 1425 } 1426 1427 // draw the selection dividers 1428 if (mSelectionDivider != null) { 1429 // draw the top divider 1430 int topOfTopDivider = mTopSelectionDividerTop; 1431 int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; 1432 mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); 1433 mSelectionDivider.draw(canvas); 1434 1435 // draw the bottom divider 1436 int bottomOfBottomDivider = mBottomSelectionDividerBottom; 1437 int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; 1438 mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); 1439 mSelectionDivider.draw(canvas); 1440 } 1441 } 1442 1443 @Override 1444 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1445 super.onInitializeAccessibilityEvent(event); 1446 event.setClassName(NumberPicker.class.getName()); 1447 event.setScrollable(true); 1448 event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); 1449 event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight); 1450 } 1451 1452 @Override 1453 public AccessibilityNodeProvider getAccessibilityNodeProvider() { 1454 if (!mHasSelectorWheel) { 1455 return super.getAccessibilityNodeProvider(); 1456 } 1457 if (mAccessibilityNodeProvider == null) { 1458 mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); 1459 } 1460 return mAccessibilityNodeProvider; 1461 } 1462 1463 /** 1464 * Makes a measure spec that tries greedily to use the max value. 1465 * 1466 * @param measureSpec The measure spec. 1467 * @param maxSize The max value for the size. 1468 * @return A measure spec greedily imposing the max size. 1469 */ 1470 private int makeMeasureSpec(int measureSpec, int maxSize) { 1471 if (maxSize == SIZE_UNSPECIFIED) { 1472 return measureSpec; 1473 } 1474 final int size = MeasureSpec.getSize(measureSpec); 1475 final int mode = MeasureSpec.getMode(measureSpec); 1476 switch (mode) { 1477 case MeasureSpec.EXACTLY: 1478 return measureSpec; 1479 case MeasureSpec.AT_MOST: 1480 return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); 1481 case MeasureSpec.UNSPECIFIED: 1482 return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); 1483 default: 1484 throw new IllegalArgumentException("Unknown measure mode: " + mode); 1485 } 1486 } 1487 1488 /** 1489 * Utility to reconcile a desired size and state, with constraints imposed 1490 * by a MeasureSpec. Tries to respect the min size, unless a different size 1491 * is imposed by the constraints. 1492 * 1493 * @param minSize The minimal desired size. 1494 * @param measuredSize The currently measured size. 1495 * @param measureSpec The current measure spec. 1496 * @return The resolved size and state. 1497 */ 1498 private int resolveSizeAndStateRespectingMinSize( 1499 int minSize, int measuredSize, int measureSpec) { 1500 if (minSize != SIZE_UNSPECIFIED) { 1501 final int desiredWidth = Math.max(minSize, measuredSize); 1502 return resolveSizeAndState(desiredWidth, measureSpec, 0); 1503 } else { 1504 return measuredSize; 1505 } 1506 } 1507 1508 /** 1509 * Resets the selector indices and clear the cached string representation of 1510 * these indices. 1511 */ 1512 private void initializeSelectorWheelIndices() { 1513 mSelectorIndexToStringCache.clear(); 1514 int[] selectorIndices = mSelectorIndices; 1515 int current = getValue(); 1516 for (int i = 0; i < mSelectorIndices.length; i++) { 1517 int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); 1518 if (mWrapSelectorWheel) { 1519 selectorIndex = getWrappedSelectorIndex(selectorIndex); 1520 } 1521 selectorIndices[i] = selectorIndex; 1522 ensureCachedScrollSelectorValue(selectorIndices[i]); 1523 } 1524 } 1525 1526 /** 1527 * Sets the current value of this NumberPicker. 1528 * 1529 * @param current The new value of the NumberPicker. 1530 * @param notifyChange Whether to notify if the current value changed. 1531 */ 1532 private void setValueInternal(int current, boolean notifyChange) { 1533 if (mValue == current) { 1534 return; 1535 } 1536 // Wrap around the values if we go past the start or end 1537 if (mWrapSelectorWheel) { 1538 current = getWrappedSelectorIndex(current); 1539 } else { 1540 current = Math.max(current, mMinValue); 1541 current = Math.min(current, mMaxValue); 1542 } 1543 int previous = mValue; 1544 mValue = current; 1545 updateInputTextView(); 1546 if (notifyChange) { 1547 notifyChange(previous, current); 1548 } 1549 initializeSelectorWheelIndices(); 1550 invalidate(); 1551 } 1552 1553 /** 1554 * Changes the current value by one which is increment or 1555 * decrement based on the passes argument. 1556 * decrement the current value. 1557 * 1558 * @param increment True to increment, false to decrement. 1559 */ 1560 private void changeValueByOne(boolean increment) { 1561 if (mHasSelectorWheel) { 1562 mInputText.setVisibility(View.INVISIBLE); 1563 if (!moveToFinalScrollerPosition(mFlingScroller)) { 1564 moveToFinalScrollerPosition(mAdjustScroller); 1565 } 1566 mPreviousScrollerY = 0; 1567 if (increment) { 1568 mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION); 1569 } else { 1570 mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION); 1571 } 1572 invalidate(); 1573 } else { 1574 if (increment) { 1575 setValueInternal(mValue + 1, true); 1576 } else { 1577 setValueInternal(mValue - 1, true); 1578 } 1579 } 1580 } 1581 1582 private void initializeSelectorWheel() { 1583 initializeSelectorWheelIndices(); 1584 int[] selectorIndices = mSelectorIndices; 1585 int totalTextHeight = selectorIndices.length * mTextSize; 1586 float totalTextGapHeight = (mBottom - mTop) - totalTextHeight; 1587 float textGapCount = selectorIndices.length; 1588 mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f); 1589 mSelectorElementHeight = mTextSize + mSelectorTextGapHeight; 1590 // Ensure that the middle item is positioned the same as the text in 1591 // mInputText 1592 int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop(); 1593 mInitialScrollOffset = editTextTextPosition 1594 - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); 1595 mCurrentScrollOffset = mInitialScrollOffset; 1596 updateInputTextView(); 1597 } 1598 1599 private void initializeFadingEdges() { 1600 setVerticalFadingEdgeEnabled(true); 1601 setFadingEdgeLength((mBottom - mTop - mTextSize) / 2); 1602 } 1603 1604 /** 1605 * Callback invoked upon completion of a given <code>scroller</code>. 1606 */ 1607 private void onScrollerFinished(Scroller scroller) { 1608 if (scroller == mFlingScroller) { 1609 if (!ensureScrollWheelAdjusted()) { 1610 updateInputTextView(); 1611 } 1612 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 1613 } else { 1614 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 1615 updateInputTextView(); 1616 } 1617 } 1618 } 1619 1620 /** 1621 * Handles transition to a given <code>scrollState</code> 1622 */ 1623 private void onScrollStateChange(int scrollState) { 1624 if (mScrollState == scrollState) { 1625 return; 1626 } 1627 mScrollState = scrollState; 1628 if (mOnScrollListener != null) { 1629 mOnScrollListener.onScrollStateChange(this, scrollState); 1630 } 1631 } 1632 1633 /** 1634 * Flings the selector with the given <code>velocityY</code>. 1635 */ 1636 private void fling(int velocityY) { 1637 mPreviousScrollerY = 0; 1638 1639 if (velocityY > 0) { 1640 mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1641 } else { 1642 mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1643 } 1644 1645 invalidate(); 1646 } 1647 1648 /** 1649 * @return The wrapped index <code>selectorIndex</code> value. 1650 */ 1651 private int getWrappedSelectorIndex(int selectorIndex) { 1652 if (selectorIndex > mMaxValue) { 1653 return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1; 1654 } else if (selectorIndex < mMinValue) { 1655 return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1; 1656 } 1657 return selectorIndex; 1658 } 1659 1660 /** 1661 * Increments the <code>selectorIndices</code> whose string representations 1662 * will be displayed in the selector. 1663 */ 1664 private void incrementSelectorIndices(int[] selectorIndices) { 1665 for (int i = 0; i < selectorIndices.length - 1; i++) { 1666 selectorIndices[i] = selectorIndices[i + 1]; 1667 } 1668 int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; 1669 if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) { 1670 nextScrollSelectorIndex = mMinValue; 1671 } 1672 selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; 1673 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1674 } 1675 1676 /** 1677 * Decrements the <code>selectorIndices</code> whose string representations 1678 * will be displayed in the selector. 1679 */ 1680 private void decrementSelectorIndices(int[] selectorIndices) { 1681 for (int i = selectorIndices.length - 1; i > 0; i--) { 1682 selectorIndices[i] = selectorIndices[i - 1]; 1683 } 1684 int nextScrollSelectorIndex = selectorIndices[1] - 1; 1685 if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) { 1686 nextScrollSelectorIndex = mMaxValue; 1687 } 1688 selectorIndices[0] = nextScrollSelectorIndex; 1689 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1690 } 1691 1692 /** 1693 * Ensures we have a cached string representation of the given <code> 1694 * selectorIndex</code> to avoid multiple instantiations of the same string. 1695 */ 1696 private void ensureCachedScrollSelectorValue(int selectorIndex) { 1697 SparseArray<String> cache = mSelectorIndexToStringCache; 1698 String scrollSelectorValue = cache.get(selectorIndex); 1699 if (scrollSelectorValue != null) { 1700 return; 1701 } 1702 if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { 1703 scrollSelectorValue = ""; 1704 } else { 1705 if (mDisplayedValues != null) { 1706 int displayedValueIndex = selectorIndex - mMinValue; 1707 scrollSelectorValue = mDisplayedValues[displayedValueIndex]; 1708 } else { 1709 scrollSelectorValue = formatNumber(selectorIndex); 1710 } 1711 } 1712 cache.put(selectorIndex, scrollSelectorValue); 1713 } 1714 1715 private String formatNumber(int value) { 1716 return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value); 1717 } 1718 1719 private void validateInputTextView(View v) { 1720 String str = String.valueOf(((TextView) v).getText()); 1721 if (TextUtils.isEmpty(str)) { 1722 // Restore to the old value as we don't allow empty values 1723 updateInputTextView(); 1724 } else { 1725 // Check the new value and ensure it's in range 1726 int current = getSelectedPos(str.toString()); 1727 setValueInternal(current, true); 1728 } 1729 } 1730 1731 /** 1732 * Updates the view of this NumberPicker. If displayValues were specified in 1733 * the string corresponding to the index specified by the current value will 1734 * be returned. Otherwise, the formatter specified in {@link #setFormatter} 1735 * will be used to format the number. 1736 * 1737 * @return Whether the text was updated. 1738 */ 1739 private boolean updateInputTextView() { 1740 /* 1741 * If we don't have displayed values then use the current number else 1742 * find the correct value in the displayed values for the current 1743 * number. 1744 */ 1745 String text = (mDisplayedValues == null) ? formatNumber(mValue) 1746 : mDisplayedValues[mValue - mMinValue]; 1747 if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) { 1748 mInputText.setText(text); 1749 return true; 1750 } 1751 1752 return false; 1753 } 1754 1755 /** 1756 * Notifies the listener, if registered, of a change of the value of this 1757 * NumberPicker. 1758 */ 1759 private void notifyChange(int previous, int current) { 1760 if (mOnValueChangeListener != null) { 1761 mOnValueChangeListener.onValueChange(this, previous, mValue); 1762 } 1763 } 1764 1765 /** 1766 * Posts a command for changing the current value by one. 1767 * 1768 * @param increment Whether to increment or decrement the value. 1769 */ 1770 private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { 1771 if (mChangeCurrentByOneFromLongPressCommand == null) { 1772 mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); 1773 } else { 1774 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 1775 } 1776 mChangeCurrentByOneFromLongPressCommand.setStep(increment); 1777 postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis); 1778 } 1779 1780 /** 1781 * Removes the command for changing the current value by one. 1782 */ 1783 private void removeChangeCurrentByOneFromLongPress() { 1784 if (mChangeCurrentByOneFromLongPressCommand != null) { 1785 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 1786 } 1787 } 1788 1789 /** 1790 * Posts a command for beginning an edit of the current value via IME on 1791 * long press. 1792 */ 1793 private void postBeginSoftInputOnLongPressCommand() { 1794 if (mBeginSoftInputOnLongPressCommand == null) { 1795 mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand(); 1796 } else { 1797 removeCallbacks(mBeginSoftInputOnLongPressCommand); 1798 } 1799 postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout()); 1800 } 1801 1802 /** 1803 * Removes the command for beginning an edit of the current value via IME. 1804 */ 1805 private void removeBeginSoftInputCommand() { 1806 if (mBeginSoftInputOnLongPressCommand != null) { 1807 removeCallbacks(mBeginSoftInputOnLongPressCommand); 1808 } 1809 } 1810 1811 /** 1812 * Removes all pending callback from the message queue. 1813 */ 1814 private void removeAllCallbacks() { 1815 if (mChangeCurrentByOneFromLongPressCommand != null) { 1816 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 1817 } 1818 if (mSetSelectionCommand != null) { 1819 removeCallbacks(mSetSelectionCommand); 1820 } 1821 if (mBeginSoftInputOnLongPressCommand != null) { 1822 removeCallbacks(mBeginSoftInputOnLongPressCommand); 1823 } 1824 mPressedStateHelper.cancel(); 1825 } 1826 1827 /** 1828 * @return The selected index given its displayed <code>value</code>. 1829 */ 1830 private int getSelectedPos(String value) { 1831 if (mDisplayedValues == null) { 1832 try { 1833 return Integer.parseInt(value); 1834 } catch (NumberFormatException e) { 1835 // Ignore as if it's not a number we don't care 1836 } 1837 } else { 1838 for (int i = 0; i < mDisplayedValues.length; i++) { 1839 // Don't force the user to type in jan when ja will do 1840 value = value.toLowerCase(); 1841 if (mDisplayedValues[i].toLowerCase().startsWith(value)) { 1842 return mMinValue + i; 1843 } 1844 } 1845 1846 /* 1847 * The user might have typed in a number into the month field i.e. 1848 * 10 instead of OCT so support that too. 1849 */ 1850 try { 1851 return Integer.parseInt(value); 1852 } catch (NumberFormatException e) { 1853 1854 // Ignore as if it's not a number we don't care 1855 } 1856 } 1857 return mMinValue; 1858 } 1859 1860 /** 1861 * Posts an {@link SetSelectionCommand} from the given <code>selectionStart 1862 * </code> to <code>selectionEnd</code>. 1863 */ 1864 private void postSetSelectionCommand(int selectionStart, int selectionEnd) { 1865 if (mSetSelectionCommand == null) { 1866 mSetSelectionCommand = new SetSelectionCommand(); 1867 } else { 1868 removeCallbacks(mSetSelectionCommand); 1869 } 1870 mSetSelectionCommand.mSelectionStart = selectionStart; 1871 mSetSelectionCommand.mSelectionEnd = selectionEnd; 1872 post(mSetSelectionCommand); 1873 } 1874 1875 /** 1876 * The numbers accepted by the input text's {@link Filter} 1877 */ 1878 private static final char[] DIGIT_CHARACTERS = new char[] { 1879 // Latin digits are the common case 1880 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 1881 // Arabic-Indic 1882 '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668' 1883 , '\u0669', 1884 // Extended Arabic-Indic 1885 '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8' 1886 , '\u06f9' 1887 }; 1888 1889 /** 1890 * Filter for accepting only valid indices or prefixes of the string 1891 * representation of valid indices. 1892 */ 1893 class InputTextFilter extends NumberKeyListener { 1894 1895 // XXX This doesn't allow for range limits when controlled by a 1896 // soft input method! 1897 public int getInputType() { 1898 return InputType.TYPE_CLASS_TEXT; 1899 } 1900 1901 @Override 1902 protected char[] getAcceptedChars() { 1903 return DIGIT_CHARACTERS; 1904 } 1905 1906 @Override 1907 public CharSequence filter( 1908 CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { 1909 if (mDisplayedValues == null) { 1910 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); 1911 if (filtered == null) { 1912 filtered = source.subSequence(start, end); 1913 } 1914 1915 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1916 + dest.subSequence(dend, dest.length()); 1917 1918 if ("".equals(result)) { 1919 return result; 1920 } 1921 int val = getSelectedPos(result); 1922 1923 /* 1924 * Ensure the user can't type in a value greater than the max 1925 * allowed. We have to allow less than min as the user might 1926 * want to delete some numbers and then type a new number. 1927 */ 1928 if (val > mMaxValue) { 1929 return ""; 1930 } else { 1931 return filtered; 1932 } 1933 } else { 1934 CharSequence filtered = String.valueOf(source.subSequence(start, end)); 1935 if (TextUtils.isEmpty(filtered)) { 1936 return ""; 1937 } 1938 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 1939 + dest.subSequence(dend, dest.length()); 1940 String str = String.valueOf(result).toLowerCase(); 1941 for (String val : mDisplayedValues) { 1942 String valLowerCase = val.toLowerCase(); 1943 if (valLowerCase.startsWith(str)) { 1944 postSetSelectionCommand(result.length(), val.length()); 1945 return val.subSequence(dstart, val.length()); 1946 } 1947 } 1948 return ""; 1949 } 1950 } 1951 } 1952 1953 /** 1954 * Ensures that the scroll wheel is adjusted i.e. there is no offset and the 1955 * middle element is in the middle of the widget. 1956 * 1957 * @return Whether an adjustment has been made. 1958 */ 1959 private boolean ensureScrollWheelAdjusted() { 1960 // adjust to the closest value 1961 int deltaY = mInitialScrollOffset - mCurrentScrollOffset; 1962 if (deltaY != 0) { 1963 mPreviousScrollerY = 0; 1964 if (Math.abs(deltaY) > mSelectorElementHeight / 2) { 1965 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; 1966 } 1967 mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); 1968 invalidate(); 1969 return true; 1970 } 1971 return false; 1972 } 1973 1974 class PressedStateHelper implements Runnable { 1975 public static final int BUTTON_INCREMENT = 1; 1976 public static final int BUTTON_DECREMENT = 2; 1977 1978 private final int MODE_PRESS = 1; 1979 private final int MODE_TAPPED = 2; 1980 1981 private int mManagedButton; 1982 private int mMode; 1983 1984 public void cancel() { 1985 mMode = 0; 1986 mManagedButton = 0; 1987 NumberPicker.this.removeCallbacks(this); 1988 if (mIncrementVirtualButtonPressed) { 1989 mIncrementVirtualButtonPressed = false; 1990 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 1991 } 1992 mDecrementVirtualButtonPressed = false; 1993 if (mDecrementVirtualButtonPressed) { 1994 invalidate(0, 0, mRight, mTopSelectionDividerTop); 1995 } 1996 } 1997 1998 public void buttonPressDelayed(int button) { 1999 cancel(); 2000 mMode = MODE_PRESS; 2001 mManagedButton = button; 2002 NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout()); 2003 } 2004 2005 public void buttonTapped(int button) { 2006 cancel(); 2007 mMode = MODE_TAPPED; 2008 mManagedButton = button; 2009 NumberPicker.this.post(this); 2010 } 2011 2012 @Override 2013 public void run() { 2014 switch (mMode) { 2015 case MODE_PRESS: { 2016 switch (mManagedButton) { 2017 case BUTTON_INCREMENT: { 2018 mIncrementVirtualButtonPressed = true; 2019 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2020 } break; 2021 case BUTTON_DECREMENT: { 2022 mDecrementVirtualButtonPressed = true; 2023 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2024 } 2025 } 2026 } break; 2027 case MODE_TAPPED: { 2028 switch (mManagedButton) { 2029 case BUTTON_INCREMENT: { 2030 if (!mIncrementVirtualButtonPressed) { 2031 NumberPicker.this.postDelayed(this, 2032 ViewConfiguration.getPressedStateDuration()); 2033 } 2034 mIncrementVirtualButtonPressed ^= true; 2035 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2036 } break; 2037 case BUTTON_DECREMENT: { 2038 if (!mDecrementVirtualButtonPressed) { 2039 NumberPicker.this.postDelayed(this, 2040 ViewConfiguration.getPressedStateDuration()); 2041 } 2042 mDecrementVirtualButtonPressed ^= true; 2043 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2044 } 2045 } 2046 } break; 2047 } 2048 } 2049 } 2050 2051 /** 2052 * Command for setting the input text selection. 2053 */ 2054 class SetSelectionCommand implements Runnable { 2055 private int mSelectionStart; 2056 2057 private int mSelectionEnd; 2058 2059 public void run() { 2060 mInputText.setSelection(mSelectionStart, mSelectionEnd); 2061 } 2062 } 2063 2064 /** 2065 * Command for changing the current value from a long press by one. 2066 */ 2067 class ChangeCurrentByOneFromLongPressCommand implements Runnable { 2068 private boolean mIncrement; 2069 2070 private void setStep(boolean increment) { 2071 mIncrement = increment; 2072 } 2073 2074 @Override 2075 public void run() { 2076 changeValueByOne(mIncrement); 2077 postDelayed(this, mLongPressUpdateInterval); 2078 } 2079 } 2080 2081 /** 2082 * @hide 2083 */ 2084 public static class CustomEditText extends EditText { 2085 2086 public CustomEditText(Context context, AttributeSet attrs) { 2087 super(context, attrs); 2088 } 2089 2090 @Override 2091 public void onEditorAction(int actionCode) { 2092 super.onEditorAction(actionCode); 2093 if (actionCode == EditorInfo.IME_ACTION_DONE) { 2094 clearFocus(); 2095 } 2096 } 2097 } 2098 2099 /** 2100 * Command for beginning soft input on long press. 2101 */ 2102 class BeginSoftInputOnLongPressCommand implements Runnable { 2103 2104 @Override 2105 public void run() { 2106 showSoftInput(); 2107 mIngonreMoveEvents = true; 2108 } 2109 } 2110 2111 /** 2112 * Class for managing virtual view tree rooted at this picker. 2113 */ 2114 class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider { 2115 private static final int UNDEFINED = Integer.MIN_VALUE; 2116 2117 private static final int VIRTUAL_VIEW_ID_INCREMENT = 1; 2118 2119 private static final int VIRTUAL_VIEW_ID_INPUT = 2; 2120 2121 private static final int VIRTUAL_VIEW_ID_DECREMENT = 3; 2122 2123 private final Rect mTempRect = new Rect(); 2124 2125 private final int[] mTempArray = new int[2]; 2126 2127 private int mAccessibilityFocusedView = UNDEFINED; 2128 2129 @Override 2130 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 2131 switch (virtualViewId) { 2132 case View.NO_ID: 2133 return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY, 2134 mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); 2135 case VIRTUAL_VIEW_ID_DECREMENT: 2136 return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT, 2137 getVirtualDecrementButtonText(), mScrollX, mScrollY, 2138 mScrollX + (mRight - mLeft), 2139 mTopSelectionDividerTop + mSelectionDividerHeight); 2140 case VIRTUAL_VIEW_ID_INPUT: 2141 return createAccessibiltyNodeInfoForInputText(); 2142 case VIRTUAL_VIEW_ID_INCREMENT: 2143 return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT, 2144 getVirtualIncrementButtonText(), mScrollX, 2145 mBottomSelectionDividerBottom - mSelectionDividerHeight, 2146 mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); 2147 } 2148 return super.createAccessibilityNodeInfo(virtualViewId); 2149 } 2150 2151 @Override 2152 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched, 2153 int virtualViewId) { 2154 if (TextUtils.isEmpty(searched)) { 2155 return Collections.emptyList(); 2156 } 2157 String searchedLowerCase = searched.toLowerCase(); 2158 List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>(); 2159 switch (virtualViewId) { 2160 case View.NO_ID: { 2161 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, 2162 VIRTUAL_VIEW_ID_DECREMENT, result); 2163 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, 2164 VIRTUAL_VIEW_ID_INPUT, result); 2165 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, 2166 VIRTUAL_VIEW_ID_INCREMENT, result); 2167 return result; 2168 } 2169 case VIRTUAL_VIEW_ID_DECREMENT: 2170 case VIRTUAL_VIEW_ID_INCREMENT: 2171 case VIRTUAL_VIEW_ID_INPUT: { 2172 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId, 2173 result); 2174 return result; 2175 } 2176 } 2177 return super.findAccessibilityNodeInfosByText(searched, virtualViewId); 2178 } 2179 2180 @Override 2181 public boolean performAction(int virtualViewId, int action, Bundle arguments) { 2182 switch (virtualViewId) { 2183 case View.NO_ID: { 2184 switch (action) { 2185 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2186 if (mAccessibilityFocusedView != virtualViewId) { 2187 mAccessibilityFocusedView = virtualViewId; 2188 requestAccessibilityFocus(); 2189 return true; 2190 } 2191 } return false; 2192 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2193 if (mAccessibilityFocusedView == virtualViewId) { 2194 mAccessibilityFocusedView = UNDEFINED; 2195 clearAccessibilityFocus(); 2196 return true; 2197 } 2198 return false; 2199 } 2200 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 2201 if (NumberPicker.this.isEnabled() 2202 && (getWrapSelectorWheel() || getValue() < getMaxValue())) { 2203 changeValueByOne(true); 2204 return true; 2205 } 2206 } return false; 2207 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 2208 if (NumberPicker.this.isEnabled() 2209 && (getWrapSelectorWheel() || getValue() > getMinValue())) { 2210 changeValueByOne(false); 2211 return true; 2212 } 2213 } return false; 2214 } 2215 } break; 2216 case VIRTUAL_VIEW_ID_INPUT: { 2217 switch (action) { 2218 case AccessibilityNodeInfo.ACTION_FOCUS: { 2219 if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) { 2220 return mInputText.requestFocus(); 2221 } 2222 } break; 2223 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { 2224 if (NumberPicker.this.isEnabled() && mInputText.isFocused()) { 2225 mInputText.clearFocus(); 2226 return true; 2227 } 2228 return false; 2229 } 2230 case AccessibilityNodeInfo.ACTION_CLICK: { 2231 if (NumberPicker.this.isEnabled()) { 2232 showSoftInput(); 2233 return true; 2234 } 2235 return false; 2236 } 2237 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2238 if (mAccessibilityFocusedView != virtualViewId) { 2239 mAccessibilityFocusedView = virtualViewId; 2240 sendAccessibilityEventForVirtualView(virtualViewId, 2241 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 2242 mInputText.invalidate(); 2243 return true; 2244 } 2245 } return false; 2246 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2247 if (mAccessibilityFocusedView == virtualViewId) { 2248 mAccessibilityFocusedView = UNDEFINED; 2249 sendAccessibilityEventForVirtualView(virtualViewId, 2250 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 2251 mInputText.invalidate(); 2252 return true; 2253 } 2254 } return false; 2255 default: { 2256 return mInputText.performAccessibilityAction(action, arguments); 2257 } 2258 } 2259 } return false; 2260 case VIRTUAL_VIEW_ID_INCREMENT: { 2261 switch (action) { 2262 case AccessibilityNodeInfo.ACTION_CLICK: { 2263 if (NumberPicker.this.isEnabled()) { 2264 NumberPicker.this.changeValueByOne(true); 2265 sendAccessibilityEventForVirtualView(virtualViewId, 2266 AccessibilityEvent.TYPE_VIEW_CLICKED); 2267 return true; 2268 } 2269 } return false; 2270 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2271 if (mAccessibilityFocusedView != virtualViewId) { 2272 mAccessibilityFocusedView = virtualViewId; 2273 sendAccessibilityEventForVirtualView(virtualViewId, 2274 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 2275 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2276 return true; 2277 } 2278 } return false; 2279 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2280 if (mAccessibilityFocusedView == virtualViewId) { 2281 mAccessibilityFocusedView = UNDEFINED; 2282 sendAccessibilityEventForVirtualView(virtualViewId, 2283 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 2284 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2285 return true; 2286 } 2287 } return false; 2288 } 2289 } return false; 2290 case VIRTUAL_VIEW_ID_DECREMENT: { 2291 switch (action) { 2292 case AccessibilityNodeInfo.ACTION_CLICK: { 2293 if (NumberPicker.this.isEnabled()) { 2294 final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT); 2295 NumberPicker.this.changeValueByOne(increment); 2296 sendAccessibilityEventForVirtualView(virtualViewId, 2297 AccessibilityEvent.TYPE_VIEW_CLICKED); 2298 return true; 2299 } 2300 } return false; 2301 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2302 if (mAccessibilityFocusedView != virtualViewId) { 2303 mAccessibilityFocusedView = virtualViewId; 2304 sendAccessibilityEventForVirtualView(virtualViewId, 2305 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 2306 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2307 return true; 2308 } 2309 } return false; 2310 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2311 if (mAccessibilityFocusedView == virtualViewId) { 2312 mAccessibilityFocusedView = UNDEFINED; 2313 sendAccessibilityEventForVirtualView(virtualViewId, 2314 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 2315 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2316 return true; 2317 } 2318 } return false; 2319 } 2320 } return false; 2321 } 2322 return super.performAction(virtualViewId, action, arguments); 2323 } 2324 2325 public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) { 2326 switch (virtualViewId) { 2327 case VIRTUAL_VIEW_ID_DECREMENT: { 2328 if (hasVirtualDecrementButton()) { 2329 sendAccessibilityEventForVirtualButton(virtualViewId, eventType, 2330 getVirtualDecrementButtonText()); 2331 } 2332 } break; 2333 case VIRTUAL_VIEW_ID_INPUT: { 2334 sendAccessibilityEventForVirtualText(eventType); 2335 } break; 2336 case VIRTUAL_VIEW_ID_INCREMENT: { 2337 if (hasVirtualIncrementButton()) { 2338 sendAccessibilityEventForVirtualButton(virtualViewId, eventType, 2339 getVirtualIncrementButtonText()); 2340 } 2341 } break; 2342 } 2343 } 2344 2345 private void sendAccessibilityEventForVirtualText(int eventType) { 2346 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2347 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 2348 mInputText.onInitializeAccessibilityEvent(event); 2349 mInputText.onPopulateAccessibilityEvent(event); 2350 event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); 2351 requestSendAccessibilityEvent(NumberPicker.this, event); 2352 } 2353 } 2354 2355 private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, 2356 String text) { 2357 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2358 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 2359 event.setClassName(Button.class.getName()); 2360 event.setPackageName(mContext.getPackageName()); 2361 event.getText().add(text); 2362 event.setEnabled(NumberPicker.this.isEnabled()); 2363 event.setSource(NumberPicker.this, virtualViewId); 2364 requestSendAccessibilityEvent(NumberPicker.this, event); 2365 } 2366 } 2367 2368 private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, 2369 int virtualViewId, List<AccessibilityNodeInfo> outResult) { 2370 switch (virtualViewId) { 2371 case VIRTUAL_VIEW_ID_DECREMENT: { 2372 String text = getVirtualDecrementButtonText(); 2373 if (!TextUtils.isEmpty(text) 2374 && text.toString().toLowerCase().contains(searchedLowerCase)) { 2375 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT)); 2376 } 2377 } return; 2378 case VIRTUAL_VIEW_ID_INPUT: { 2379 CharSequence text = mInputText.getText(); 2380 if (!TextUtils.isEmpty(text) && 2381 text.toString().toLowerCase().contains(searchedLowerCase)) { 2382 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); 2383 return; 2384 } 2385 CharSequence contentDesc = mInputText.getText(); 2386 if (!TextUtils.isEmpty(contentDesc) && 2387 contentDesc.toString().toLowerCase().contains(searchedLowerCase)) { 2388 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); 2389 return; 2390 } 2391 } break; 2392 case VIRTUAL_VIEW_ID_INCREMENT: { 2393 String text = getVirtualIncrementButtonText(); 2394 if (!TextUtils.isEmpty(text) 2395 && text.toString().toLowerCase().contains(searchedLowerCase)) { 2396 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT)); 2397 } 2398 } return; 2399 } 2400 } 2401 2402 private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText() { 2403 AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo(); 2404 info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); 2405 if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) { 2406 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 2407 } 2408 if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) { 2409 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 2410 } 2411 return info; 2412 } 2413 2414 private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId, 2415 String text, int left, int top, int right, int bottom) { 2416 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); 2417 info.setClassName(Button.class.getName()); 2418 info.setPackageName(mContext.getPackageName()); 2419 info.setSource(NumberPicker.this, virtualViewId); 2420 info.setParent(NumberPicker.this); 2421 info.setText(text); 2422 info.setClickable(true); 2423 info.setLongClickable(true); 2424 info.setEnabled(NumberPicker.this.isEnabled()); 2425 Rect boundsInParent = mTempRect; 2426 boundsInParent.set(left, top, right, bottom); 2427 info.setVisibleToUser(isVisibleToUser(boundsInParent)); 2428 info.setBoundsInParent(boundsInParent); 2429 Rect boundsInScreen = boundsInParent; 2430 int[] locationOnScreen = mTempArray; 2431 getLocationOnScreen(locationOnScreen); 2432 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); 2433 info.setBoundsInScreen(boundsInScreen); 2434 2435 if (mAccessibilityFocusedView != virtualViewId) { 2436 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 2437 } 2438 if (mAccessibilityFocusedView == virtualViewId) { 2439 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 2440 } 2441 if (NumberPicker.this.isEnabled()) { 2442 info.addAction(AccessibilityNodeInfo.ACTION_CLICK); 2443 } 2444 2445 return info; 2446 } 2447 2448 private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top, 2449 int right, int bottom) { 2450 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); 2451 info.setClassName(NumberPicker.class.getName()); 2452 info.setPackageName(mContext.getPackageName()); 2453 info.setSource(NumberPicker.this); 2454 2455 if (hasVirtualDecrementButton()) { 2456 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT); 2457 } 2458 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); 2459 if (hasVirtualIncrementButton()) { 2460 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT); 2461 } 2462 2463 info.setParent((View) getParentForAccessibility()); 2464 info.setEnabled(NumberPicker.this.isEnabled()); 2465 info.setScrollable(true); 2466 2467 final float applicationScale = 2468 getContext().getResources().getCompatibilityInfo().applicationScale; 2469 2470 Rect boundsInParent = mTempRect; 2471 boundsInParent.set(left, top, right, bottom); 2472 boundsInParent.scale(applicationScale); 2473 info.setBoundsInParent(boundsInParent); 2474 2475 info.setVisibleToUser(isVisibleToUser()); 2476 2477 Rect boundsInScreen = boundsInParent; 2478 int[] locationOnScreen = mTempArray; 2479 getLocationOnScreen(locationOnScreen); 2480 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); 2481 boundsInScreen.scale(applicationScale); 2482 info.setBoundsInScreen(boundsInScreen); 2483 2484 if (mAccessibilityFocusedView != View.NO_ID) { 2485 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 2486 } 2487 if (mAccessibilityFocusedView == View.NO_ID) { 2488 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 2489 } 2490 if (NumberPicker.this.isEnabled()) { 2491 if (getWrapSelectorWheel() || getValue() < getMaxValue()) { 2492 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 2493 } 2494 if (getWrapSelectorWheel() || getValue() > getMinValue()) { 2495 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 2496 } 2497 } 2498 2499 return info; 2500 } 2501 2502 private boolean hasVirtualDecrementButton() { 2503 return getWrapSelectorWheel() || getValue() > getMinValue(); 2504 } 2505 2506 private boolean hasVirtualIncrementButton() { 2507 return getWrapSelectorWheel() || getValue() < getMaxValue(); 2508 } 2509 2510 private String getVirtualDecrementButtonText() { 2511 int value = mValue - 1; 2512 if (mWrapSelectorWheel) { 2513 value = getWrappedSelectorIndex(value); 2514 } 2515 if (value >= mMinValue) { 2516 return (mDisplayedValues == null) ? formatNumber(value) 2517 : mDisplayedValues[value - mMinValue]; 2518 } 2519 return null; 2520 } 2521 2522 private String getVirtualIncrementButtonText() { 2523 int value = mValue + 1; 2524 if (mWrapSelectorWheel) { 2525 value = getWrappedSelectorIndex(value); 2526 } 2527 if (value <= mMaxValue) { 2528 return (mDisplayedValues == null) ? formatNumber(value) 2529 : mDisplayedValues[value - mMinValue]; 2530 } 2531 return null; 2532 } 2533 } 2534 2535 static private String formatNumberWithLocale(int value) { 2536 return String.format(Locale.getDefault(), "%d", value); 2537 } 2538} 2539