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