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