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