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