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