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