1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.CallSuper; 20 import android.annotation.ColorInt; 21 import android.annotation.FloatRange; 22 import android.annotation.IntDef; 23 import android.annotation.IntRange; 24 import android.annotation.Px; 25 import android.annotation.TestApi; 26 import android.annotation.Widget; 27 import android.compat.annotation.UnsupportedAppUsage; 28 import android.content.Context; 29 import android.content.res.ColorStateList; 30 import android.content.res.TypedArray; 31 import android.graphics.Canvas; 32 import android.graphics.Color; 33 import android.graphics.Paint; 34 import android.graphics.Paint.Align; 35 import android.graphics.Rect; 36 import android.graphics.drawable.Drawable; 37 import android.icu.text.DecimalFormatSymbols; 38 import android.os.Build; 39 import android.os.Bundle; 40 import android.text.InputFilter; 41 import android.text.InputType; 42 import android.text.Spanned; 43 import android.text.TextUtils; 44 import android.text.method.NumberKeyListener; 45 import android.util.AttributeSet; 46 import android.util.SparseArray; 47 import android.util.TypedValue; 48 import android.view.KeyEvent; 49 import android.view.LayoutInflater; 50 import android.view.LayoutInflater.Filter; 51 import android.view.MotionEvent; 52 import android.view.VelocityTracker; 53 import android.view.View; 54 import android.view.ViewConfiguration; 55 import android.view.accessibility.AccessibilityEvent; 56 import android.view.accessibility.AccessibilityManager; 57 import android.view.accessibility.AccessibilityNodeInfo; 58 import android.view.accessibility.AccessibilityNodeProvider; 59 import android.view.animation.DecelerateInterpolator; 60 import android.view.inputmethod.EditorInfo; 61 import android.view.inputmethod.InputMethodManager; 62 import android.widget.flags.Flags; 63 64 import com.android.internal.R; 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 DecimalFormatSymbols.getInstance(locale).getZeroDigit(); 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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 their 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 if (Flags.fixUnboldedTypefaceForNumberpicker()) { 795 paint.setTypeface(mInputText.getPaint().getTypeface()); 796 } else { 797 paint.setTypeface(mInputText.getTypeface()); 798 } 799 ColorStateList colors = mInputText.getTextColors(); 800 int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); 801 paint.setColor(color); 802 mSelectorWheelPaint = paint; 803 804 // create the fling and adjust scrollers 805 mFlingScroller = new Scroller(getContext(), null, true); 806 mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); 807 808 updateInputTextView(); 809 810 // If not explicitly specified this view is important for accessibility. 811 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 812 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 813 } 814 815 // Should be focusable by default, as the text view whose visibility changes is focusable 816 if (getFocusable() == View.FOCUSABLE_AUTO) { 817 setFocusable(View.FOCUSABLE); 818 setFocusableInTouchMode(true); 819 } 820 } 821 822 @Override onLayout(boolean changed, int left, int top, int right, int bottom)823 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 824 if (!mHasSelectorWheel) { 825 super.onLayout(changed, left, top, right, bottom); 826 return; 827 } 828 final int msrdWdth = getMeasuredWidth(); 829 final int msrdHght = getMeasuredHeight(); 830 831 // Input text centered horizontally. 832 final int inptTxtMsrdWdth = mInputText.getMeasuredWidth(); 833 final int inptTxtMsrdHght = mInputText.getMeasuredHeight(); 834 final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2; 835 final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2; 836 final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth; 837 final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; 838 mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); 839 840 if (changed) { 841 // need to do all this when we know our size 842 initializeSelectorWheel(); 843 initializeFadingEdges(); 844 mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2 845 - mSelectionDividerHeight; 846 mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight 847 + mSelectionDividersDistance; 848 } 849 } 850 851 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)852 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 853 if (!mHasSelectorWheel) { 854 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 855 return; 856 } 857 // Try greedily to fit the max width and height. 858 final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); 859 final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); 860 super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); 861 // Flag if we are measured with width or height less than the respective min. 862 final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), 863 widthMeasureSpec); 864 final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), 865 heightMeasureSpec); 866 setMeasuredDimension(widthSize, heightSize); 867 } 868 869 /** 870 * Move to the final position of a scroller. Ensures to force finish the scroller 871 * and if it is not at its final position a scroll of the selector wheel is 872 * performed to fast forward to the final position. 873 * 874 * @param scroller The scroller to whose final position to get. 875 * @return True of the a move was performed, i.e. the scroller was not in final position. 876 */ moveToFinalScrollerPosition(Scroller scroller)877 private boolean moveToFinalScrollerPosition(Scroller scroller) { 878 scroller.forceFinished(true); 879 int amountToScroll = scroller.getFinalY() - scroller.getCurrY(); 880 int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight; 881 int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; 882 if (overshootAdjustment != 0) { 883 if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) { 884 if (overshootAdjustment > 0) { 885 overshootAdjustment -= mSelectorElementHeight; 886 } else { 887 overshootAdjustment += mSelectorElementHeight; 888 } 889 } 890 amountToScroll += overshootAdjustment; 891 scrollBy(0, amountToScroll); 892 return true; 893 } 894 return false; 895 } 896 897 @Override onInterceptTouchEvent(MotionEvent event)898 public boolean onInterceptTouchEvent(MotionEvent event) { 899 if (!mHasSelectorWheel || !isEnabled()) { 900 return false; 901 } 902 final int action = event.getActionMasked(); 903 switch (action) { 904 case MotionEvent.ACTION_DOWN: { 905 removeAllCallbacks(); 906 hideSoftInput(); 907 mLastDownOrMoveEventY = mLastDownEventY = event.getY(); 908 mLastDownEventTime = event.getEventTime(); 909 mIgnoreMoveEvents = false; 910 mPerformClickOnTap = false; 911 // Handle pressed state before any state change. 912 if (mLastDownEventY < mTopSelectionDividerTop) { 913 if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { 914 mPressedStateHelper.buttonPressDelayed( 915 PressedStateHelper.BUTTON_DECREMENT); 916 } 917 } else if (mLastDownEventY > mBottomSelectionDividerBottom) { 918 if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { 919 mPressedStateHelper.buttonPressDelayed( 920 PressedStateHelper.BUTTON_INCREMENT); 921 } 922 } 923 // Make sure we support flinging inside scrollables. 924 getParent().requestDisallowInterceptTouchEvent(true); 925 if (!mFlingScroller.isFinished()) { 926 mFlingScroller.forceFinished(true); 927 mAdjustScroller.forceFinished(true); 928 onScrollerFinished(mFlingScroller); 929 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 930 } else if (!mAdjustScroller.isFinished()) { 931 mFlingScroller.forceFinished(true); 932 mAdjustScroller.forceFinished(true); 933 onScrollerFinished(mAdjustScroller); 934 } else if (mLastDownEventY < mTopSelectionDividerTop) { 935 postChangeCurrentByOneFromLongPress( 936 false, ViewConfiguration.getLongPressTimeout()); 937 } else if (mLastDownEventY > mBottomSelectionDividerBottom) { 938 postChangeCurrentByOneFromLongPress( 939 true, ViewConfiguration.getLongPressTimeout()); 940 } else { 941 mPerformClickOnTap = true; 942 postBeginSoftInputOnLongPressCommand(); 943 } 944 return true; 945 } 946 } 947 return false; 948 } 949 950 @Override onTouchEvent(MotionEvent event)951 public boolean onTouchEvent(MotionEvent event) { 952 if (!isEnabled() || !mHasSelectorWheel) { 953 return false; 954 } 955 if (mVelocityTracker == null) { 956 mVelocityTracker = VelocityTracker.obtain(); 957 } 958 mVelocityTracker.addMovement(event); 959 int action = event.getActionMasked(); 960 switch (action) { 961 case MotionEvent.ACTION_MOVE: { 962 if (mIgnoreMoveEvents) { 963 break; 964 } 965 float currentMoveY = event.getY(); 966 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 967 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); 968 if (deltaDownY > mTouchSlop) { 969 removeAllCallbacks(); 970 onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 971 } 972 } else { 973 int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY)); 974 scrollBy(0, deltaMoveY); 975 invalidate(); 976 } 977 mLastDownOrMoveEventY = currentMoveY; 978 } break; 979 case MotionEvent.ACTION_UP: { 980 removeBeginSoftInputCommand(); 981 removeChangeCurrentByOneFromLongPress(); 982 mPressedStateHelper.cancel(); 983 VelocityTracker velocityTracker = mVelocityTracker; 984 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 985 int initialVelocity = (int) velocityTracker.getYVelocity(); 986 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { 987 fling(initialVelocity); 988 onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 989 } else { 990 int eventY = (int) event.getY(); 991 int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY); 992 long deltaTime = event.getEventTime() - mLastDownEventTime; 993 if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) { 994 if (mPerformClickOnTap) { 995 mPerformClickOnTap = false; 996 performClick(); 997 } else { 998 int selectorIndexOffset = (eventY / mSelectorElementHeight) 999 - SELECTOR_MIDDLE_ITEM_INDEX; 1000 if (selectorIndexOffset > 0) { 1001 changeValueByOne(true); 1002 mPressedStateHelper.buttonTapped( 1003 PressedStateHelper.BUTTON_INCREMENT); 1004 } else if (selectorIndexOffset < 0) { 1005 changeValueByOne(false); 1006 mPressedStateHelper.buttonTapped( 1007 PressedStateHelper.BUTTON_DECREMENT); 1008 } 1009 } 1010 } else { 1011 ensureScrollWheelAdjusted(); 1012 } 1013 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 1014 } 1015 mVelocityTracker.recycle(); 1016 mVelocityTracker = null; 1017 } break; 1018 } 1019 return true; 1020 } 1021 1022 @Override dispatchTouchEvent(MotionEvent event)1023 public boolean dispatchTouchEvent(MotionEvent event) { 1024 final int action = event.getActionMasked(); 1025 switch (action) { 1026 case MotionEvent.ACTION_CANCEL: 1027 case MotionEvent.ACTION_UP: 1028 removeAllCallbacks(); 1029 break; 1030 } 1031 return super.dispatchTouchEvent(event); 1032 } 1033 1034 @Override dispatchKeyEvent(KeyEvent event)1035 public boolean dispatchKeyEvent(KeyEvent event) { 1036 final int keyCode = event.getKeyCode(); 1037 switch (keyCode) { 1038 case KeyEvent.KEYCODE_DPAD_CENTER: 1039 case KeyEvent.KEYCODE_ENTER: 1040 case KeyEvent.KEYCODE_NUMPAD_ENTER: 1041 removeAllCallbacks(); 1042 break; 1043 case KeyEvent.KEYCODE_DPAD_DOWN: 1044 case KeyEvent.KEYCODE_DPAD_UP: 1045 if (!mHasSelectorWheel) { 1046 break; 1047 } 1048 switch (event.getAction()) { 1049 case KeyEvent.ACTION_DOWN: 1050 if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) 1051 ? getValue() < getMaxValue() : getValue() > getMinValue())) { 1052 requestFocus(); 1053 mLastHandledDownDpadKeyCode = keyCode; 1054 removeAllCallbacks(); 1055 if (mFlingScroller.isFinished()) { 1056 changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN); 1057 } 1058 return true; 1059 } 1060 break; 1061 case KeyEvent.ACTION_UP: 1062 if (mLastHandledDownDpadKeyCode == keyCode) { 1063 mLastHandledDownDpadKeyCode = -1; 1064 return true; 1065 } 1066 break; 1067 } 1068 } 1069 return super.dispatchKeyEvent(event); 1070 } 1071 1072 @Override dispatchTrackballEvent(MotionEvent event)1073 public boolean dispatchTrackballEvent(MotionEvent event) { 1074 final int action = event.getActionMasked(); 1075 switch (action) { 1076 case MotionEvent.ACTION_CANCEL: 1077 case MotionEvent.ACTION_UP: 1078 removeAllCallbacks(); 1079 break; 1080 } 1081 return super.dispatchTrackballEvent(event); 1082 } 1083 1084 @Override dispatchHoverEvent(MotionEvent event)1085 protected boolean dispatchHoverEvent(MotionEvent event) { 1086 if (!mHasSelectorWheel) { 1087 return super.dispatchHoverEvent(event); 1088 } 1089 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 1090 final int eventY = (int) event.getY(); 1091 final int hoveredVirtualViewId; 1092 if (eventY < mTopSelectionDividerTop) { 1093 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT; 1094 } else if (eventY > mBottomSelectionDividerBottom) { 1095 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT; 1096 } else { 1097 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT; 1098 } 1099 final int action = event.getActionMasked(); 1100 AccessibilityNodeProviderImpl provider = 1101 (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider(); 1102 switch (action) { 1103 case MotionEvent.ACTION_HOVER_ENTER: { 1104 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, 1105 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 1106 mLastHoveredChildVirtualViewId = hoveredVirtualViewId; 1107 provider.performAction(hoveredVirtualViewId, 1108 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 1109 } break; 1110 case MotionEvent.ACTION_HOVER_MOVE: { 1111 if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId 1112 && mLastHoveredChildVirtualViewId != View.NO_ID) { 1113 provider.sendAccessibilityEventForVirtualView( 1114 mLastHoveredChildVirtualViewId, 1115 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 1116 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, 1117 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 1118 mLastHoveredChildVirtualViewId = hoveredVirtualViewId; 1119 provider.performAction(hoveredVirtualViewId, 1120 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 1121 } 1122 } break; 1123 case MotionEvent.ACTION_HOVER_EXIT: { 1124 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, 1125 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 1126 mLastHoveredChildVirtualViewId = View.NO_ID; 1127 } break; 1128 } 1129 } 1130 return false; 1131 } 1132 1133 @Override computeScroll()1134 public void computeScroll() { 1135 Scroller scroller = mFlingScroller; 1136 if (scroller.isFinished()) { 1137 scroller = mAdjustScroller; 1138 if (scroller.isFinished()) { 1139 return; 1140 } 1141 } 1142 scroller.computeScrollOffset(); 1143 int currentScrollerY = scroller.getCurrY(); 1144 if (mPreviousScrollerY == 0) { 1145 mPreviousScrollerY = scroller.getStartY(); 1146 } 1147 scrollBy(0, currentScrollerY - mPreviousScrollerY); 1148 mPreviousScrollerY = currentScrollerY; 1149 if (scroller.isFinished()) { 1150 onScrollerFinished(scroller); 1151 } else { 1152 invalidate(); 1153 } 1154 } 1155 1156 @Override setEnabled(boolean enabled)1157 public void setEnabled(boolean enabled) { 1158 super.setEnabled(enabled); 1159 if (!mHasSelectorWheel) { 1160 mIncrementButton.setEnabled(enabled); 1161 } 1162 if (!mHasSelectorWheel) { 1163 mDecrementButton.setEnabled(enabled); 1164 } 1165 mInputText.setEnabled(enabled); 1166 } 1167 1168 @Override scrollBy(int x, int y)1169 public void scrollBy(int x, int y) { 1170 int[] selectorIndices = mSelectorIndices; 1171 int startScrollOffset = mCurrentScrollOffset; 1172 if (!mWrapSelectorWheel && y > 0 1173 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 1174 mCurrentScrollOffset = mInitialScrollOffset; 1175 return; 1176 } 1177 if (!mWrapSelectorWheel && y < 0 1178 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 1179 mCurrentScrollOffset = mInitialScrollOffset; 1180 return; 1181 } 1182 mCurrentScrollOffset += y; 1183 while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { 1184 mCurrentScrollOffset -= mSelectorElementHeight; 1185 decrementSelectorIndices(selectorIndices); 1186 setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); 1187 if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { 1188 mCurrentScrollOffset = mInitialScrollOffset; 1189 } 1190 } 1191 while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { 1192 mCurrentScrollOffset += mSelectorElementHeight; 1193 incrementSelectorIndices(selectorIndices); 1194 setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); 1195 if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { 1196 mCurrentScrollOffset = mInitialScrollOffset; 1197 } 1198 } 1199 if (startScrollOffset != mCurrentScrollOffset) { 1200 onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset); 1201 } 1202 } 1203 1204 @Override computeVerticalScrollOffset()1205 protected int computeVerticalScrollOffset() { 1206 return mCurrentScrollOffset; 1207 } 1208 1209 @Override computeVerticalScrollRange()1210 protected int computeVerticalScrollRange() { 1211 return (mMaxValue - mMinValue + 1) * mSelectorElementHeight; 1212 } 1213 1214 @Override computeVerticalScrollExtent()1215 protected int computeVerticalScrollExtent() { 1216 return getHeight(); 1217 } 1218 1219 @Override getSolidColor()1220 public int getSolidColor() { 1221 return mSolidColor; 1222 } 1223 1224 /** 1225 * Sets the listener to be notified on change of the current value. 1226 * 1227 * @param onValueChangedListener The listener. 1228 */ setOnValueChangedListener(OnValueChangeListener onValueChangedListener)1229 public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { 1230 mOnValueChangeListener = onValueChangedListener; 1231 } 1232 1233 /** 1234 * Set listener to be notified for scroll state changes. 1235 * 1236 * @param onScrollListener The listener. 1237 */ setOnScrollListener(OnScrollListener onScrollListener)1238 public void setOnScrollListener(OnScrollListener onScrollListener) { 1239 mOnScrollListener = onScrollListener; 1240 } 1241 1242 /** 1243 * Set the formatter to be used for formatting the current value. 1244 * <p> 1245 * Note: If you have provided alternative values for the values this 1246 * formatter is never invoked. 1247 * </p> 1248 * 1249 * @param formatter The formatter object. If formatter is <code>null</code>, 1250 * {@link String#valueOf(int)} will be used. 1251 *@see #setDisplayedValues(String[]) 1252 */ setFormatter(Formatter formatter)1253 public void setFormatter(Formatter formatter) { 1254 if (formatter == mFormatter) { 1255 return; 1256 } 1257 mFormatter = formatter; 1258 initializeSelectorWheelIndices(); 1259 updateInputTextView(); 1260 } 1261 1262 /** 1263 * Set the current value for the number picker. 1264 * <p> 1265 * If the argument is less than the {@link NumberPicker#getMinValue()} and 1266 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 1267 * current value is set to the {@link NumberPicker#getMinValue()} value. 1268 * </p> 1269 * <p> 1270 * If the argument is less than the {@link NumberPicker#getMinValue()} and 1271 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 1272 * current value is set to the {@link NumberPicker#getMaxValue()} value. 1273 * </p> 1274 * <p> 1275 * If the argument is more than the {@link NumberPicker#getMaxValue()} and 1276 * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the 1277 * current value is set to the {@link NumberPicker#getMaxValue()} value. 1278 * </p> 1279 * <p> 1280 * If the argument is more than the {@link NumberPicker#getMaxValue()} and 1281 * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the 1282 * current value is set to the {@link NumberPicker#getMinValue()} value. 1283 * </p> 1284 * 1285 * @param value The current value. 1286 * @see #setWrapSelectorWheel(boolean) 1287 * @see #setMinValue(int) 1288 * @see #setMaxValue(int) 1289 */ setValue(int value)1290 public void setValue(int value) { 1291 setValueInternal(value, false); 1292 } 1293 1294 @Override performClick()1295 public boolean performClick() { 1296 if (!mHasSelectorWheel) { 1297 return super.performClick(); 1298 } else if (!super.performClick()) { 1299 showSoftInput(); 1300 } 1301 return true; 1302 } 1303 1304 @Override performLongClick()1305 public boolean performLongClick() { 1306 if (!mHasSelectorWheel) { 1307 return super.performLongClick(); 1308 } else if (!super.performLongClick()) { 1309 showSoftInput(); 1310 mIgnoreMoveEvents = true; 1311 } 1312 return true; 1313 } 1314 1315 /** 1316 * Shows the soft input for its input text. 1317 */ showSoftInput()1318 private void showSoftInput() { 1319 InputMethodManager inputMethodManager = 1320 getContext().getSystemService(InputMethodManager.class); 1321 if (inputMethodManager != null) { 1322 if (mHasSelectorWheel) { 1323 mInputText.setVisibility(View.VISIBLE); 1324 } 1325 mInputText.requestFocus(); 1326 inputMethodManager.showSoftInput(mInputText, 0); 1327 } 1328 } 1329 1330 /** 1331 * Hides the soft input if it is active for the input text. 1332 */ hideSoftInput()1333 private void hideSoftInput() { 1334 InputMethodManager inputMethodManager = 1335 getContext().getSystemService(InputMethodManager.class); 1336 if (inputMethodManager != null) { 1337 inputMethodManager.hideSoftInputFromView(mInputText, 0); 1338 } 1339 if (mHasSelectorWheel) { 1340 mInputText.setVisibility(View.INVISIBLE); 1341 } 1342 } 1343 1344 /** 1345 * Computes the max width if no such specified as an attribute. 1346 */ tryComputeMaxWidth()1347 private void tryComputeMaxWidth() { 1348 if (!mComputeMaxWidth) { 1349 return; 1350 } 1351 int maxTextWidth = 0; 1352 if (mDisplayedValues == null) { 1353 float maxDigitWidth = 0; 1354 for (int i = 0; i <= 9; i++) { 1355 final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i)); 1356 if (digitWidth > maxDigitWidth) { 1357 maxDigitWidth = digitWidth; 1358 } 1359 } 1360 int numberOfDigits = 0; 1361 int current = mMaxValue; 1362 while (current > 0) { 1363 numberOfDigits++; 1364 current = current / 10; 1365 } 1366 maxTextWidth = (int) (numberOfDigits * maxDigitWidth); 1367 } else { 1368 final int valueCount = mDisplayedValues.length; 1369 for (int i = 0; i < valueCount; i++) { 1370 final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]); 1371 if (textWidth > maxTextWidth) { 1372 maxTextWidth = (int) textWidth; 1373 } 1374 } 1375 } 1376 maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight(); 1377 if (mMaxWidth != maxTextWidth) { 1378 if (maxTextWidth > mMinWidth) { 1379 mMaxWidth = maxTextWidth; 1380 } else { 1381 mMaxWidth = mMinWidth; 1382 } 1383 invalidate(); 1384 } 1385 } 1386 1387 /** 1388 * Gets whether the selector wheel wraps when reaching the min/max value. 1389 * 1390 * @return True if the selector wheel wraps. 1391 * 1392 * @see #getMinValue() 1393 * @see #getMaxValue() 1394 */ getWrapSelectorWheel()1395 public boolean getWrapSelectorWheel() { 1396 return mWrapSelectorWheel; 1397 } 1398 1399 /** 1400 * Sets whether the selector wheel shown during flinging/scrolling should 1401 * wrap around the {@link NumberPicker#getMinValue()} and 1402 * {@link NumberPicker#getMaxValue()} values. 1403 * <p> 1404 * By default if the range (max - min) is more than the number of items shown 1405 * on the selector wheel the selector wheel wrapping is enabled. 1406 * </p> 1407 * <p> 1408 * <strong>Note:</strong> If the number of items, i.e. the range ( 1409 * {@link #getMaxValue()} - {@link #getMinValue()}) is less than 1410 * the number of items shown on the selector wheel, the selector wheel will 1411 * not wrap. Hence, in such a case calling this method is a NOP. 1412 * </p> 1413 * 1414 * @param wrapSelectorWheel Whether to wrap. 1415 */ setWrapSelectorWheel(boolean wrapSelectorWheel)1416 public void setWrapSelectorWheel(boolean wrapSelectorWheel) { 1417 mWrapSelectorWheelPreferred = wrapSelectorWheel; 1418 updateWrapSelectorWheel(); 1419 1420 } 1421 1422 /** 1423 * Whether or not the selector wheel should be wrapped is determined by user choice and whether 1424 * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the 1425 * latter is calculated based on min & max value set vs selector's visual length. Therefore, 1426 * this method should be called any time any of the 3 values (i.e. user choice, min and max 1427 * value) gets updated. 1428 */ updateWrapSelectorWheel()1429 private void updateWrapSelectorWheel() { 1430 final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; 1431 mWrapSelectorWheel = wrappingAllowed && mWrapSelectorWheelPreferred; 1432 } 1433 1434 /** 1435 * Sets the speed at which the numbers be incremented and decremented when 1436 * the up and down buttons are long pressed respectively. 1437 * <p> 1438 * The default value is 300 ms. 1439 * </p> 1440 * 1441 * @param intervalMillis The speed (in milliseconds) at which the numbers 1442 * will be incremented and decremented. 1443 */ setOnLongPressUpdateInterval(long intervalMillis)1444 public void setOnLongPressUpdateInterval(long intervalMillis) { 1445 mLongPressUpdateInterval = intervalMillis; 1446 } 1447 1448 /** 1449 * Returns the value of the picker. 1450 * 1451 * @return The value. 1452 */ getValue()1453 public int getValue() { 1454 return mValue; 1455 } 1456 1457 /** 1458 * Returns the min value of the picker. 1459 * 1460 * @return The min value 1461 */ getMinValue()1462 public int getMinValue() { 1463 return mMinValue; 1464 } 1465 1466 /** 1467 * Sets the min value of the picker. 1468 * 1469 * @param minValue The min value inclusive. 1470 * 1471 * <strong>Note:</strong> The length of the displayed values array 1472 * set via {@link #setDisplayedValues(String[])} must be equal to the 1473 * range of selectable numbers which is equal to 1474 * {@link #getMaxValue()} - {@link #getMinValue()} + 1. 1475 */ setMinValue(int minValue)1476 public void setMinValue(int minValue) { 1477 if (mMinValue == minValue) { 1478 return; 1479 } 1480 if (minValue < 0) { 1481 throw new IllegalArgumentException("minValue must be >= 0"); 1482 } 1483 mMinValue = minValue; 1484 if (mMinValue > mValue) { 1485 mValue = mMinValue; 1486 } 1487 updateWrapSelectorWheel(); 1488 initializeSelectorWheelIndices(); 1489 updateInputTextView(); 1490 tryComputeMaxWidth(); 1491 invalidate(); 1492 } 1493 1494 /** 1495 * Returns the max value of the picker. 1496 * 1497 * @return The max value. 1498 */ getMaxValue()1499 public int getMaxValue() { 1500 return mMaxValue; 1501 } 1502 1503 /** 1504 * Sets the max value of the picker. 1505 * 1506 * @param maxValue The max value inclusive. 1507 * 1508 * <strong>Note:</strong> The length of the displayed values array 1509 * set via {@link #setDisplayedValues(String[])} must be equal to the 1510 * range of selectable numbers which is equal to 1511 * {@link #getMaxValue()} - {@link #getMinValue()} + 1. 1512 */ setMaxValue(int maxValue)1513 public void setMaxValue(int maxValue) { 1514 if (mMaxValue == maxValue) { 1515 return; 1516 } 1517 if (maxValue < 0) { 1518 throw new IllegalArgumentException("maxValue must be >= 0"); 1519 } 1520 mMaxValue = maxValue; 1521 if (mMaxValue < mValue) { 1522 mValue = mMaxValue; 1523 } 1524 updateWrapSelectorWheel(); 1525 initializeSelectorWheelIndices(); 1526 updateInputTextView(); 1527 tryComputeMaxWidth(); 1528 invalidate(); 1529 } 1530 1531 /** 1532 * Gets the values to be displayed instead of string values. 1533 * 1534 * @return The displayed values. 1535 */ getDisplayedValues()1536 public String[] getDisplayedValues() { 1537 return mDisplayedValues; 1538 } 1539 1540 /** 1541 * Sets the values to be displayed. 1542 * 1543 * @param displayedValues The displayed values. 1544 * 1545 * <strong>Note:</strong> The length of the displayed values array 1546 * must be equal to the range of selectable numbers which is equal to 1547 * {@link #getMaxValue()} - {@link #getMinValue()} + 1. 1548 */ setDisplayedValues(String[] displayedValues)1549 public void setDisplayedValues(String[] displayedValues) { 1550 if (mDisplayedValues == displayedValues) { 1551 return; 1552 } 1553 mDisplayedValues = displayedValues; 1554 if (mDisplayedValues != null) { 1555 // Allow text entry rather than strictly numeric entry. 1556 mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT 1557 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 1558 } else { 1559 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 1560 } 1561 updateInputTextView(); 1562 initializeSelectorWheelIndices(); 1563 tryComputeMaxWidth(); 1564 } 1565 1566 /** 1567 * Retrieves the displayed value for the current selection in this picker. 1568 * 1569 * @hide 1570 */ 1571 @TestApi getDisplayedValueForCurrentSelection()1572 public CharSequence getDisplayedValueForCurrentSelection() { 1573 // The cache field itself is initialized at declaration time, and since it's final, it 1574 // can't be null here. The cache is updated in ensureCachedScrollSelectorValue which is 1575 // called, directly or indirectly, on every call to setDisplayedValues, setFormatter, 1576 // setMinValue, setMaxValue and setValue, as well as user-driven interaction with the 1577 // picker. As such, the contents of the cache are always synced to the latest state of 1578 // the widget. 1579 return mSelectorIndexToStringCache.get(getValue()); 1580 } 1581 1582 /** 1583 * Set the height for the divider that separates the currently selected value from the others. 1584 * @param height The height to be set 1585 */ setSelectionDividerHeight(@ntRangefrom = 0) @x int height)1586 public void setSelectionDividerHeight(@IntRange(from = 0) @Px int height) { 1587 mSelectionDividerHeight = height; 1588 invalidate(); 1589 } 1590 1591 /** 1592 * Retrieve the height for the divider that separates the currently selected value from the 1593 * others. 1594 * @return The height of the divider 1595 */ 1596 @Px getSelectionDividerHeight()1597 public int getSelectionDividerHeight() { 1598 return mSelectionDividerHeight; 1599 } 1600 1601 @Override getTopFadingEdgeStrength()1602 protected float getTopFadingEdgeStrength() { 1603 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1604 } 1605 1606 @Override getBottomFadingEdgeStrength()1607 protected float getBottomFadingEdgeStrength() { 1608 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; 1609 } 1610 1611 @Override onDetachedFromWindow()1612 protected void onDetachedFromWindow() { 1613 super.onDetachedFromWindow(); 1614 removeAllCallbacks(); 1615 } 1616 1617 @CallSuper 1618 @Override drawableStateChanged()1619 protected void drawableStateChanged() { 1620 super.drawableStateChanged(); 1621 1622 final Drawable selectionDivider = mSelectionDivider; 1623 if (selectionDivider != null && selectionDivider.isStateful() 1624 && selectionDivider.setState(getDrawableState())) { 1625 invalidateDrawable(selectionDivider); 1626 } 1627 } 1628 1629 @CallSuper 1630 @Override jumpDrawablesToCurrentState()1631 public void jumpDrawablesToCurrentState() { 1632 super.jumpDrawablesToCurrentState(); 1633 1634 if (mSelectionDivider != null) { 1635 mSelectionDivider.jumpToCurrentState(); 1636 } 1637 } 1638 1639 /** @hide */ 1640 @Override onResolveDrawables(@esolvedLayoutDir int layoutDirection)1641 public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { 1642 super.onResolveDrawables(layoutDirection); 1643 1644 if (mSelectionDivider != null) { 1645 mSelectionDivider.setLayoutDirection(layoutDirection); 1646 } 1647 } 1648 1649 @Override onDraw(Canvas canvas)1650 protected void onDraw(Canvas canvas) { 1651 if (!mHasSelectorWheel) { 1652 super.onDraw(canvas); 1653 return; 1654 } 1655 final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true; 1656 float x = (mRight - mLeft) / 2; 1657 float y = mCurrentScrollOffset; 1658 1659 // draw the virtual buttons pressed state if needed 1660 if (showSelectorWheel && mVirtualButtonPressedDrawable != null 1661 && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { 1662 if (mDecrementVirtualButtonPressed) { 1663 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); 1664 mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop); 1665 mVirtualButtonPressedDrawable.draw(canvas); 1666 } 1667 if (mIncrementVirtualButtonPressed) { 1668 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); 1669 mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight, 1670 mBottom); 1671 mVirtualButtonPressedDrawable.draw(canvas); 1672 } 1673 } 1674 1675 // draw the selector wheel 1676 int[] selectorIndices = mSelectorIndices; 1677 for (int i = 0; i < selectorIndices.length; i++) { 1678 int selectorIndex = selectorIndices[i]; 1679 String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); 1680 // Do not draw the middle item if input is visible since the input 1681 // is shown only if the wheel is static and it covers the middle 1682 // item. Otherwise, if the user starts editing the text via the 1683 // IME they may see a dimmed version of the old value intermixed 1684 // with the new one. 1685 if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) || 1686 (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) { 1687 canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); 1688 } 1689 y += mSelectorElementHeight; 1690 } 1691 1692 // draw the selection dividers 1693 if (showSelectorWheel && mSelectionDivider != null) { 1694 // draw the top divider 1695 int topOfTopDivider = mTopSelectionDividerTop; 1696 int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; 1697 mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); 1698 mSelectionDivider.draw(canvas); 1699 1700 // draw the bottom divider 1701 int bottomOfBottomDivider = mBottomSelectionDividerBottom; 1702 int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; 1703 mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); 1704 mSelectionDivider.draw(canvas); 1705 } 1706 } 1707 1708 /** @hide */ 1709 @Override onInitializeAccessibilityEventInternal(AccessibilityEvent event)1710 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { 1711 super.onInitializeAccessibilityEventInternal(event); 1712 event.setClassName(NumberPicker.class.getName()); 1713 event.setScrollable(true); 1714 event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); 1715 event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight); 1716 } 1717 1718 @Override getAccessibilityNodeProvider()1719 public AccessibilityNodeProvider getAccessibilityNodeProvider() { 1720 if (!mHasSelectorWheel) { 1721 return super.getAccessibilityNodeProvider(); 1722 } 1723 if (mAccessibilityNodeProvider == null) { 1724 mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); 1725 } 1726 return mAccessibilityNodeProvider; 1727 } 1728 1729 /** 1730 * Sets the text color for all the states (normal, selected, focused) to be the given color. 1731 * 1732 * @param color A color value in the form 0xAARRGGBB. 1733 */ setTextColor(@olorInt int color)1734 public void setTextColor(@ColorInt int color) { 1735 mSelectorWheelPaint.setColor(color); 1736 mInputText.setTextColor(color); 1737 invalidate(); 1738 } 1739 1740 /** 1741 * @return the text color. 1742 */ 1743 @ColorInt getTextColor()1744 public int getTextColor() { 1745 return mSelectorWheelPaint.getColor(); 1746 } 1747 1748 /** 1749 * Sets the text size to the given value. This value must be > 0 1750 * 1751 * @param size The size in pixel units. 1752 */ setTextSize(@loatRangefrom = 0.0, fromInclusive = false) float size)1753 public void setTextSize(@FloatRange(from = 0.0, fromInclusive = false) float size) { 1754 mSelectorWheelPaint.setTextSize(size); 1755 mInputText.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); 1756 invalidate(); 1757 } 1758 1759 /** 1760 * @return the size (in pixels) of the text size in this NumberPicker. 1761 */ 1762 @FloatRange(from = 0.0, fromInclusive = false) getTextSize()1763 public float getTextSize() { 1764 return mSelectorWheelPaint.getTextSize(); 1765 } 1766 1767 /** 1768 * Makes a measure spec that tries greedily to use the max value. 1769 * 1770 * @param measureSpec The measure spec. 1771 * @param maxSize The max value for the size. 1772 * @return A measure spec greedily imposing the max size. 1773 */ makeMeasureSpec(int measureSpec, int maxSize)1774 private int makeMeasureSpec(int measureSpec, int maxSize) { 1775 if (maxSize == SIZE_UNSPECIFIED) { 1776 return measureSpec; 1777 } 1778 final int size = MeasureSpec.getSize(measureSpec); 1779 final int mode = MeasureSpec.getMode(measureSpec); 1780 switch (mode) { 1781 case MeasureSpec.EXACTLY: 1782 return measureSpec; 1783 case MeasureSpec.AT_MOST: 1784 return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); 1785 case MeasureSpec.UNSPECIFIED: 1786 return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); 1787 default: 1788 throw new IllegalArgumentException("Unknown measure mode: " + mode); 1789 } 1790 } 1791 1792 /** 1793 * Utility to reconcile a desired size and state, with constraints imposed 1794 * by a MeasureSpec. Tries to respect the min size, unless a different size 1795 * is imposed by the constraints. 1796 * 1797 * @param minSize The minimal desired size. 1798 * @param measuredSize The currently measured size. 1799 * @param measureSpec The current measure spec. 1800 * @return The resolved size and state. 1801 */ resolveSizeAndStateRespectingMinSize( int minSize, int measuredSize, int measureSpec)1802 private int resolveSizeAndStateRespectingMinSize( 1803 int minSize, int measuredSize, int measureSpec) { 1804 if (minSize != SIZE_UNSPECIFIED) { 1805 final int desiredWidth = Math.max(minSize, measuredSize); 1806 return resolveSizeAndState(desiredWidth, measureSpec, 0); 1807 } else { 1808 return measuredSize; 1809 } 1810 } 1811 1812 /** 1813 * Resets the selector indices and clear the cached string representation of 1814 * these indices. 1815 */ 1816 @UnsupportedAppUsage initializeSelectorWheelIndices()1817 private void initializeSelectorWheelIndices() { 1818 mSelectorIndexToStringCache.clear(); 1819 int[] selectorIndices = mSelectorIndices; 1820 int current = getValue(); 1821 for (int i = 0; i < mSelectorIndices.length; i++) { 1822 int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); 1823 if (mWrapSelectorWheel) { 1824 selectorIndex = getWrappedSelectorIndex(selectorIndex); 1825 } 1826 selectorIndices[i] = selectorIndex; 1827 ensureCachedScrollSelectorValue(selectorIndices[i]); 1828 } 1829 } 1830 1831 /** 1832 * Sets the current value of this NumberPicker. 1833 * 1834 * @param current The new value of the NumberPicker. 1835 * @param notifyChange Whether to notify if the current value changed. 1836 */ setValueInternal(int current, boolean notifyChange)1837 private void setValueInternal(int current, boolean notifyChange) { 1838 if (mValue == current) { 1839 return; 1840 } 1841 // Wrap around the values if we go past the start or end 1842 if (mWrapSelectorWheel) { 1843 current = getWrappedSelectorIndex(current); 1844 } else { 1845 current = Math.max(current, mMinValue); 1846 current = Math.min(current, mMaxValue); 1847 } 1848 int previous = mValue; 1849 mValue = current; 1850 // If we're flinging, we'll update the text view at the end when it becomes visible 1851 if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) { 1852 updateInputTextView(); 1853 } 1854 if (notifyChange) { 1855 notifyChange(previous, current); 1856 } 1857 initializeSelectorWheelIndices(); 1858 invalidate(); 1859 } 1860 1861 /** 1862 * Changes the current value by one which is increment or 1863 * decrement based on the passes argument. 1864 * decrement the current value. 1865 * 1866 * @param increment True to increment, false to decrement. 1867 */ 1868 @UnsupportedAppUsage changeValueByOne(boolean increment)1869 private void changeValueByOne(boolean increment) { 1870 if (mHasSelectorWheel) { 1871 hideSoftInput(); 1872 if (!moveToFinalScrollerPosition(mFlingScroller)) { 1873 moveToFinalScrollerPosition(mAdjustScroller); 1874 } 1875 mPreviousScrollerY = 0; 1876 if (increment) { 1877 mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION); 1878 } else { 1879 mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION); 1880 } 1881 invalidate(); 1882 } else { 1883 if (increment) { 1884 setValueInternal(mValue + 1, true); 1885 } else { 1886 setValueInternal(mValue - 1, true); 1887 } 1888 } 1889 } 1890 initializeSelectorWheel()1891 private void initializeSelectorWheel() { 1892 initializeSelectorWheelIndices(); 1893 int[] selectorIndices = mSelectorIndices; 1894 int totalTextHeight = selectorIndices.length * mTextSize; 1895 float totalTextGapHeight = (mBottom - mTop) - totalTextHeight; 1896 float textGapCount = selectorIndices.length; 1897 mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f); 1898 mSelectorElementHeight = mTextSize + mSelectorTextGapHeight; 1899 // Ensure that the middle item is positioned the same as the text in 1900 // mInputText 1901 int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop(); 1902 mInitialScrollOffset = editTextTextPosition 1903 - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); 1904 mCurrentScrollOffset = mInitialScrollOffset; 1905 updateInputTextView(); 1906 } 1907 initializeFadingEdges()1908 private void initializeFadingEdges() { 1909 setVerticalFadingEdgeEnabled(true); 1910 setFadingEdgeLength((mBottom - mTop - mTextSize) / 2); 1911 } 1912 1913 /** 1914 * Callback invoked upon completion of a given <code>scroller</code>. 1915 */ onScrollerFinished(Scroller scroller)1916 private void onScrollerFinished(Scroller scroller) { 1917 if (scroller == mFlingScroller) { 1918 ensureScrollWheelAdjusted(); 1919 updateInputTextView(); 1920 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 1921 } else { 1922 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 1923 updateInputTextView(); 1924 } 1925 } 1926 } 1927 1928 /** 1929 * Handles transition to a given <code>scrollState</code> 1930 */ onScrollStateChange(int scrollState)1931 private void onScrollStateChange(int scrollState) { 1932 if (mScrollState == scrollState) { 1933 return; 1934 } 1935 mScrollState = scrollState; 1936 if (mOnScrollListener != null) { 1937 mOnScrollListener.onScrollStateChange(this, scrollState); 1938 } 1939 } 1940 1941 /** 1942 * Flings the selector with the given <code>velocityY</code>. 1943 */ fling(int velocityY)1944 private void fling(int velocityY) { 1945 mPreviousScrollerY = 0; 1946 1947 if (velocityY > 0) { 1948 mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1949 } else { 1950 mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); 1951 } 1952 1953 invalidate(); 1954 } 1955 1956 /** 1957 * @return The wrapped index <code>selectorIndex</code> value. 1958 */ getWrappedSelectorIndex(int selectorIndex)1959 private int getWrappedSelectorIndex(int selectorIndex) { 1960 if (selectorIndex > mMaxValue) { 1961 return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1; 1962 } else if (selectorIndex < mMinValue) { 1963 return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1; 1964 } 1965 return selectorIndex; 1966 } 1967 1968 /** 1969 * Increments the <code>selectorIndices</code> whose string representations 1970 * will be displayed in the selector. 1971 */ incrementSelectorIndices(int[] selectorIndices)1972 private void incrementSelectorIndices(int[] selectorIndices) { 1973 for (int i = 0; i < selectorIndices.length - 1; i++) { 1974 selectorIndices[i] = selectorIndices[i + 1]; 1975 } 1976 int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; 1977 if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) { 1978 nextScrollSelectorIndex = mMinValue; 1979 } 1980 selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; 1981 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1982 } 1983 1984 /** 1985 * Decrements the <code>selectorIndices</code> whose string representations 1986 * will be displayed in the selector. 1987 */ decrementSelectorIndices(int[] selectorIndices)1988 private void decrementSelectorIndices(int[] selectorIndices) { 1989 for (int i = selectorIndices.length - 1; i > 0; i--) { 1990 selectorIndices[i] = selectorIndices[i - 1]; 1991 } 1992 int nextScrollSelectorIndex = selectorIndices[1] - 1; 1993 if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) { 1994 nextScrollSelectorIndex = mMaxValue; 1995 } 1996 selectorIndices[0] = nextScrollSelectorIndex; 1997 ensureCachedScrollSelectorValue(nextScrollSelectorIndex); 1998 } 1999 2000 /** 2001 * Ensures we have a cached string representation of the given <code> 2002 * selectorIndex</code> to avoid multiple instantiations of the same string. 2003 */ ensureCachedScrollSelectorValue(int selectorIndex)2004 private void ensureCachedScrollSelectorValue(int selectorIndex) { 2005 SparseArray<String> cache = mSelectorIndexToStringCache; 2006 String scrollSelectorValue = cache.get(selectorIndex); 2007 if (scrollSelectorValue != null) { 2008 return; 2009 } 2010 if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { 2011 scrollSelectorValue = ""; 2012 } else { 2013 if (mDisplayedValues != null) { 2014 int displayedValueIndex = selectorIndex - mMinValue; 2015 scrollSelectorValue = mDisplayedValues[displayedValueIndex]; 2016 } else { 2017 scrollSelectorValue = formatNumber(selectorIndex); 2018 } 2019 } 2020 cache.put(selectorIndex, scrollSelectorValue); 2021 } 2022 formatNumber(int value)2023 private String formatNumber(int value) { 2024 return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value); 2025 } 2026 validateInputTextView(View v)2027 private void validateInputTextView(View v) { 2028 String str = String.valueOf(((TextView) v).getText()); 2029 if (TextUtils.isEmpty(str)) { 2030 // Restore to the old value as we don't allow empty values 2031 updateInputTextView(); 2032 } else { 2033 // Check the new value and ensure it's in range 2034 int current = getSelectedPos(str.toString()); 2035 setValueInternal(current, true); 2036 } 2037 } 2038 2039 /** 2040 * Updates the view of this NumberPicker. If displayValues were specified in 2041 * the string corresponding to the index specified by the current value will 2042 * be returned. Otherwise, the formatter specified in {@link #setFormatter} 2043 * will be used to format the number. 2044 * 2045 * @return Whether the text was updated. 2046 */ updateInputTextView()2047 private boolean updateInputTextView() { 2048 /* 2049 * If we don't have displayed values then use the current number else 2050 * find the correct value in the displayed values for the current 2051 * number. 2052 */ 2053 String text = (mDisplayedValues == null) ? formatNumber(mValue) 2054 : mDisplayedValues[mValue - mMinValue]; 2055 if (!TextUtils.isEmpty(text)) { 2056 CharSequence beforeText = mInputText.getText(); 2057 if (!text.equals(beforeText.toString())) { 2058 mInputText.setText(text); 2059 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2060 AccessibilityEvent event = AccessibilityEvent.obtain( 2061 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 2062 mInputText.onInitializeAccessibilityEvent(event); 2063 mInputText.onPopulateAccessibilityEvent(event); 2064 event.setFromIndex(0); 2065 event.setRemovedCount(beforeText.length()); 2066 event.setAddedCount(text.length()); 2067 event.setBeforeText(beforeText); 2068 event.setSource(NumberPicker.this, 2069 AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT); 2070 requestSendAccessibilityEvent(NumberPicker.this, event); 2071 } 2072 return true; 2073 } 2074 } 2075 2076 return false; 2077 } 2078 2079 /** 2080 * Notifies the listener, if registered, of a change of the value of this 2081 * NumberPicker. 2082 */ notifyChange(int previous, int current)2083 private void notifyChange(int previous, int current) { 2084 if (mOnValueChangeListener != null) { 2085 mOnValueChangeListener.onValueChange(this, previous, mValue); 2086 } 2087 } 2088 2089 /** 2090 * Posts a command for changing the current value by one. 2091 * 2092 * @param increment Whether to increment or decrement the value. 2093 */ postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis)2094 private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { 2095 if (mChangeCurrentByOneFromLongPressCommand == null) { 2096 mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); 2097 } else { 2098 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 2099 } 2100 mChangeCurrentByOneFromLongPressCommand.setStep(increment); 2101 postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis); 2102 } 2103 2104 /** 2105 * Removes the command for changing the current value by one. 2106 */ removeChangeCurrentByOneFromLongPress()2107 private void removeChangeCurrentByOneFromLongPress() { 2108 if (mChangeCurrentByOneFromLongPressCommand != null) { 2109 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 2110 } 2111 } 2112 2113 /** 2114 * Posts a command for beginning an edit of the current value via IME on 2115 * long press. 2116 */ postBeginSoftInputOnLongPressCommand()2117 private void postBeginSoftInputOnLongPressCommand() { 2118 if (mBeginSoftInputOnLongPressCommand == null) { 2119 mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand(); 2120 } else { 2121 removeCallbacks(mBeginSoftInputOnLongPressCommand); 2122 } 2123 postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout()); 2124 } 2125 2126 /** 2127 * Removes the command for beginning an edit of the current value via IME. 2128 */ removeBeginSoftInputCommand()2129 private void removeBeginSoftInputCommand() { 2130 if (mBeginSoftInputOnLongPressCommand != null) { 2131 removeCallbacks(mBeginSoftInputOnLongPressCommand); 2132 } 2133 } 2134 2135 /** 2136 * Removes all pending callback from the message queue. 2137 */ removeAllCallbacks()2138 private void removeAllCallbacks() { 2139 if (mChangeCurrentByOneFromLongPressCommand != null) { 2140 removeCallbacks(mChangeCurrentByOneFromLongPressCommand); 2141 } 2142 if (mSetSelectionCommand != null) { 2143 mSetSelectionCommand.cancel(); 2144 } 2145 if (mBeginSoftInputOnLongPressCommand != null) { 2146 removeCallbacks(mBeginSoftInputOnLongPressCommand); 2147 } 2148 mPressedStateHelper.cancel(); 2149 } 2150 2151 /** 2152 * @return The selected index given its displayed <code>value</code>. 2153 */ getSelectedPos(String value)2154 private int getSelectedPos(String value) { 2155 if (mDisplayedValues == null) { 2156 try { 2157 return Integer.parseInt(value); 2158 } catch (NumberFormatException e) { 2159 // Ignore as if it's not a number we don't care 2160 } 2161 } else { 2162 for (int i = 0; i < mDisplayedValues.length; i++) { 2163 // Don't force the user to type in jan when ja will do 2164 value = value.toLowerCase(); 2165 if (mDisplayedValues[i].toLowerCase().startsWith(value)) { 2166 return mMinValue + i; 2167 } 2168 } 2169 2170 /* 2171 * The user might have typed in a number into the month field i.e. 2172 * 10 instead of OCT so support that too. 2173 */ 2174 try { 2175 return Integer.parseInt(value); 2176 } catch (NumberFormatException e) { 2177 2178 // Ignore as if it's not a number we don't care 2179 } 2180 } 2181 return mMinValue; 2182 } 2183 2184 /** 2185 * Posts a {@link SetSelectionCommand} from the given 2186 * {@code selectionStart} to {@code selectionEnd}. 2187 */ postSetSelectionCommand(int selectionStart, int selectionEnd)2188 private void postSetSelectionCommand(int selectionStart, int selectionEnd) { 2189 if (mSetSelectionCommand == null) { 2190 mSetSelectionCommand = new SetSelectionCommand(mInputText); 2191 } 2192 mSetSelectionCommand.post(selectionStart, selectionEnd); 2193 } 2194 2195 /** 2196 * The numbers accepted by the input text's {@link Filter} 2197 */ 2198 private static final char[] DIGIT_CHARACTERS = new char[] { 2199 // Latin digits are the common case 2200 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 2201 // Arabic-Indic 2202 '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668' 2203 , '\u0669', 2204 // Extended Arabic-Indic 2205 '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8' 2206 , '\u06f9', 2207 // Hindi and Marathi (Devanagari script) 2208 '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e' 2209 , '\u096f', 2210 // Bengali 2211 '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee' 2212 , '\u09ef', 2213 // Kannada 2214 '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee' 2215 , '\u0cef' 2216 }; 2217 2218 /** 2219 * Filter for accepting only valid indices or prefixes of the string 2220 * representation of valid indices. 2221 */ 2222 class InputTextFilter extends NumberKeyListener { 2223 2224 // XXX This doesn't allow for range limits when controlled by a 2225 // soft input method! getInputType()2226 public int getInputType() { 2227 return InputType.TYPE_CLASS_TEXT; 2228 } 2229 2230 @Override getAcceptedChars()2231 protected char[] getAcceptedChars() { 2232 return DIGIT_CHARACTERS; 2233 } 2234 2235 @Override filter( CharSequence source, int start, int end, Spanned dest, int dstart, int dend)2236 public CharSequence filter( 2237 CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { 2238 // We don't know what the output will be, so always cancel any 2239 // pending set selection command. 2240 if (mSetSelectionCommand != null) { 2241 mSetSelectionCommand.cancel(); 2242 } 2243 2244 if (mDisplayedValues == null) { 2245 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); 2246 if (filtered == null) { 2247 filtered = source.subSequence(start, end); 2248 } 2249 2250 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 2251 + dest.subSequence(dend, dest.length()); 2252 2253 if ("".equals(result)) { 2254 return result; 2255 } 2256 int val = getSelectedPos(result); 2257 2258 /* 2259 * Ensure the user can't type in a value greater than the max 2260 * allowed. We have to allow less than min as the user might 2261 * want to delete some numbers and then type a new number. 2262 * And prevent multiple-"0" that exceeds the length of upper 2263 * bound number. 2264 */ 2265 if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) { 2266 return ""; 2267 } else { 2268 return filtered; 2269 } 2270 } else { 2271 CharSequence filtered = String.valueOf(source.subSequence(start, end)); 2272 if (TextUtils.isEmpty(filtered)) { 2273 return ""; 2274 } 2275 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered 2276 + dest.subSequence(dend, dest.length()); 2277 String str = String.valueOf(result).toLowerCase(); 2278 for (String val : mDisplayedValues) { 2279 String valLowerCase = val.toLowerCase(); 2280 if (valLowerCase.startsWith(str)) { 2281 postSetSelectionCommand(result.length(), val.length()); 2282 return val.subSequence(dstart, val.length()); 2283 } 2284 } 2285 return ""; 2286 } 2287 } 2288 } 2289 2290 /** 2291 * Ensures that the scroll wheel is adjusted i.e. there is no offset and the 2292 * middle element is in the middle of the widget. 2293 * 2294 * @return Whether an adjustment has been made. 2295 */ ensureScrollWheelAdjusted()2296 private boolean ensureScrollWheelAdjusted() { 2297 // adjust to the closest value 2298 int deltaY = mInitialScrollOffset - mCurrentScrollOffset; 2299 if (deltaY != 0) { 2300 mPreviousScrollerY = 0; 2301 if (Math.abs(deltaY) > mSelectorElementHeight / 2) { 2302 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; 2303 } 2304 mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); 2305 invalidate(); 2306 return true; 2307 } 2308 return false; 2309 } 2310 2311 class PressedStateHelper implements Runnable { 2312 public static final int BUTTON_INCREMENT = 1; 2313 public static final int BUTTON_DECREMENT = 2; 2314 2315 private final int MODE_PRESS = 1; 2316 private final int MODE_TAPPED = 2; 2317 2318 private int mManagedButton; 2319 private int mMode; 2320 cancel()2321 public void cancel() { 2322 mMode = 0; 2323 mManagedButton = 0; 2324 NumberPicker.this.removeCallbacks(this); 2325 if (mIncrementVirtualButtonPressed) { 2326 mIncrementVirtualButtonPressed = false; 2327 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2328 } 2329 mDecrementVirtualButtonPressed = false; 2330 if (mDecrementVirtualButtonPressed) { 2331 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2332 } 2333 } 2334 buttonPressDelayed(int button)2335 public void buttonPressDelayed(int button) { 2336 cancel(); 2337 mMode = MODE_PRESS; 2338 mManagedButton = button; 2339 NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout()); 2340 } 2341 buttonTapped(int button)2342 public void buttonTapped(int button) { 2343 cancel(); 2344 mMode = MODE_TAPPED; 2345 mManagedButton = button; 2346 NumberPicker.this.post(this); 2347 } 2348 2349 @Override run()2350 public void run() { 2351 switch (mMode) { 2352 case MODE_PRESS: { 2353 switch (mManagedButton) { 2354 case BUTTON_INCREMENT: { 2355 mIncrementVirtualButtonPressed = true; 2356 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2357 } break; 2358 case BUTTON_DECREMENT: { 2359 mDecrementVirtualButtonPressed = true; 2360 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2361 } 2362 } 2363 } break; 2364 case MODE_TAPPED: { 2365 switch (mManagedButton) { 2366 case BUTTON_INCREMENT: { 2367 if (!mIncrementVirtualButtonPressed) { 2368 NumberPicker.this.postDelayed(this, 2369 ViewConfiguration.getPressedStateDuration()); 2370 } 2371 mIncrementVirtualButtonPressed ^= true; 2372 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2373 } break; 2374 case BUTTON_DECREMENT: { 2375 if (!mDecrementVirtualButtonPressed) { 2376 NumberPicker.this.postDelayed(this, 2377 ViewConfiguration.getPressedStateDuration()); 2378 } 2379 mDecrementVirtualButtonPressed ^= true; 2380 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2381 } 2382 } 2383 } break; 2384 } 2385 } 2386 } 2387 2388 /** 2389 * Command for setting the input text selection. 2390 */ 2391 private static class SetSelectionCommand implements Runnable { 2392 private final EditText mInputText; 2393 2394 private int mSelectionStart; 2395 private int mSelectionEnd; 2396 2397 /** Whether this runnable is currently posted. */ 2398 private boolean mPosted; 2399 SetSelectionCommand(EditText inputText)2400 public SetSelectionCommand(EditText inputText) { 2401 mInputText = inputText; 2402 } 2403 post(int selectionStart, int selectionEnd)2404 public void post(int selectionStart, int selectionEnd) { 2405 mSelectionStart = selectionStart; 2406 mSelectionEnd = selectionEnd; 2407 2408 if (!mPosted) { 2409 mInputText.post(this); 2410 mPosted = true; 2411 } 2412 } 2413 cancel()2414 public void cancel() { 2415 if (mPosted) { 2416 mInputText.removeCallbacks(this); 2417 mPosted = false; 2418 } 2419 } 2420 2421 @Override run()2422 public void run() { 2423 mPosted = false; 2424 mInputText.setSelection(mSelectionStart, mSelectionEnd); 2425 } 2426 } 2427 2428 /** 2429 * Command for changing the current value from a long press by one. 2430 */ 2431 class ChangeCurrentByOneFromLongPressCommand implements Runnable { 2432 private boolean mIncrement; 2433 setStep(boolean increment)2434 private void setStep(boolean increment) { 2435 mIncrement = increment; 2436 } 2437 2438 @Override run()2439 public void run() { 2440 changeValueByOne(mIncrement); 2441 postDelayed(this, mLongPressUpdateInterval); 2442 } 2443 } 2444 2445 /** 2446 * @hide 2447 */ 2448 public static class CustomEditText extends EditText { 2449 CustomEditText(Context context, AttributeSet attrs)2450 public CustomEditText(Context context, AttributeSet attrs) { 2451 super(context, attrs); 2452 } 2453 2454 @Override onEditorAction(int actionCode)2455 public void onEditorAction(int actionCode) { 2456 super.onEditorAction(actionCode); 2457 if (actionCode == EditorInfo.IME_ACTION_DONE) { 2458 clearFocus(); 2459 } 2460 } 2461 } 2462 2463 /** 2464 * Command for beginning soft input on long press. 2465 */ 2466 class BeginSoftInputOnLongPressCommand implements Runnable { 2467 2468 @Override run()2469 public void run() { 2470 performLongClick(); 2471 } 2472 } 2473 2474 /** 2475 * Class for managing virtual view tree rooted at this picker. 2476 */ 2477 class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider { 2478 private static final int UNDEFINED = Integer.MIN_VALUE; 2479 2480 private static final int VIRTUAL_VIEW_ID_INCREMENT = 1; 2481 2482 private static final int VIRTUAL_VIEW_ID_INPUT = 2; 2483 2484 private static final int VIRTUAL_VIEW_ID_DECREMENT = 3; 2485 2486 private final Rect mTempRect = new Rect(); 2487 2488 private final int[] mTempArray = new int[2]; 2489 2490 private int mAccessibilityFocusedView = UNDEFINED; 2491 2492 @Override createAccessibilityNodeInfo(int virtualViewId)2493 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 2494 switch (virtualViewId) { 2495 case View.NO_ID: 2496 return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY, 2497 mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); 2498 case VIRTUAL_VIEW_ID_DECREMENT: 2499 return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT, 2500 getVirtualDecrementButtonText(), mScrollX, mScrollY, 2501 mScrollX + (mRight - mLeft), 2502 mTopSelectionDividerTop + mSelectionDividerHeight); 2503 case VIRTUAL_VIEW_ID_INPUT: 2504 return createAccessibiltyNodeInfoForInputText(mScrollX, 2505 mTopSelectionDividerTop + mSelectionDividerHeight, 2506 mScrollX + (mRight - mLeft), 2507 mBottomSelectionDividerBottom - mSelectionDividerHeight); 2508 case VIRTUAL_VIEW_ID_INCREMENT: 2509 return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT, 2510 getVirtualIncrementButtonText(), mScrollX, 2511 mBottomSelectionDividerBottom - mSelectionDividerHeight, 2512 mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); 2513 } 2514 return super.createAccessibilityNodeInfo(virtualViewId); 2515 } 2516 2517 @Override findAccessibilityNodeInfosByText(String searched, int virtualViewId)2518 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched, 2519 int virtualViewId) { 2520 if (TextUtils.isEmpty(searched)) { 2521 return Collections.emptyList(); 2522 } 2523 String searchedLowerCase = searched.toLowerCase(); 2524 List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>(); 2525 switch (virtualViewId) { 2526 case View.NO_ID: { 2527 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, 2528 VIRTUAL_VIEW_ID_DECREMENT, result); 2529 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, 2530 VIRTUAL_VIEW_ID_INPUT, result); 2531 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, 2532 VIRTUAL_VIEW_ID_INCREMENT, result); 2533 return result; 2534 } 2535 case VIRTUAL_VIEW_ID_DECREMENT: 2536 case VIRTUAL_VIEW_ID_INCREMENT: 2537 case VIRTUAL_VIEW_ID_INPUT: { 2538 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId, 2539 result); 2540 return result; 2541 } 2542 } 2543 return super.findAccessibilityNodeInfosByText(searched, virtualViewId); 2544 } 2545 2546 @Override performAction(int virtualViewId, int action, Bundle arguments)2547 public boolean performAction(int virtualViewId, int action, Bundle arguments) { 2548 switch (virtualViewId) { 2549 case View.NO_ID: { 2550 switch (action) { 2551 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2552 if (mAccessibilityFocusedView != virtualViewId) { 2553 mAccessibilityFocusedView = virtualViewId; 2554 requestAccessibilityFocus(); 2555 return true; 2556 } 2557 } return false; 2558 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2559 if (mAccessibilityFocusedView == virtualViewId) { 2560 mAccessibilityFocusedView = UNDEFINED; 2561 clearAccessibilityFocus(); 2562 return true; 2563 } 2564 return false; 2565 } 2566 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 2567 case R.id.accessibilityActionScrollDown: { 2568 if (NumberPicker.this.isEnabled() 2569 && (getWrapSelectorWheel() || getValue() < getMaxValue())) { 2570 changeValueByOne(true); 2571 return true; 2572 } 2573 } return false; 2574 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: 2575 case R.id.accessibilityActionScrollUp: { 2576 if (NumberPicker.this.isEnabled() 2577 && (getWrapSelectorWheel() || getValue() > getMinValue())) { 2578 changeValueByOne(false); 2579 return true; 2580 } 2581 } return false; 2582 } 2583 } break; 2584 case VIRTUAL_VIEW_ID_INPUT: { 2585 switch (action) { 2586 case AccessibilityNodeInfo.ACTION_FOCUS: { 2587 if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) { 2588 return mInputText.requestFocus(); 2589 } 2590 } break; 2591 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { 2592 if (NumberPicker.this.isEnabled() && mInputText.isFocused()) { 2593 mInputText.clearFocus(); 2594 return true; 2595 } 2596 return false; 2597 } 2598 case AccessibilityNodeInfo.ACTION_CLICK: { 2599 if (NumberPicker.this.isEnabled()) { 2600 performClick(); 2601 return true; 2602 } 2603 return false; 2604 } 2605 case AccessibilityNodeInfo.ACTION_LONG_CLICK: { 2606 if (NumberPicker.this.isEnabled()) { 2607 performLongClick(); 2608 return true; 2609 } 2610 return false; 2611 } 2612 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2613 if (mAccessibilityFocusedView != virtualViewId) { 2614 mAccessibilityFocusedView = virtualViewId; 2615 sendAccessibilityEventForVirtualView(virtualViewId, 2616 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 2617 mInputText.invalidate(); 2618 return true; 2619 } 2620 } return false; 2621 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2622 if (mAccessibilityFocusedView == virtualViewId) { 2623 mAccessibilityFocusedView = UNDEFINED; 2624 sendAccessibilityEventForVirtualView(virtualViewId, 2625 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 2626 mInputText.invalidate(); 2627 return true; 2628 } 2629 } return false; 2630 default: { 2631 return mInputText.performAccessibilityAction(action, arguments); 2632 } 2633 } 2634 } return false; 2635 case VIRTUAL_VIEW_ID_INCREMENT: { 2636 switch (action) { 2637 case AccessibilityNodeInfo.ACTION_CLICK: { 2638 if (NumberPicker.this.isEnabled()) { 2639 NumberPicker.this.changeValueByOne(true); 2640 sendAccessibilityEventForVirtualView(virtualViewId, 2641 AccessibilityEvent.TYPE_VIEW_CLICKED); 2642 return true; 2643 } 2644 } return false; 2645 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2646 if (mAccessibilityFocusedView != virtualViewId) { 2647 mAccessibilityFocusedView = virtualViewId; 2648 sendAccessibilityEventForVirtualView(virtualViewId, 2649 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 2650 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2651 return true; 2652 } 2653 } return false; 2654 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2655 if (mAccessibilityFocusedView == virtualViewId) { 2656 mAccessibilityFocusedView = UNDEFINED; 2657 sendAccessibilityEventForVirtualView(virtualViewId, 2658 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 2659 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); 2660 return true; 2661 } 2662 } return false; 2663 } 2664 } return false; 2665 case VIRTUAL_VIEW_ID_DECREMENT: { 2666 switch (action) { 2667 case AccessibilityNodeInfo.ACTION_CLICK: { 2668 if (NumberPicker.this.isEnabled()) { 2669 final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT); 2670 NumberPicker.this.changeValueByOne(increment); 2671 sendAccessibilityEventForVirtualView(virtualViewId, 2672 AccessibilityEvent.TYPE_VIEW_CLICKED); 2673 return true; 2674 } 2675 } return false; 2676 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 2677 if (mAccessibilityFocusedView != virtualViewId) { 2678 mAccessibilityFocusedView = virtualViewId; 2679 sendAccessibilityEventForVirtualView(virtualViewId, 2680 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 2681 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2682 return true; 2683 } 2684 } return false; 2685 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 2686 if (mAccessibilityFocusedView == virtualViewId) { 2687 mAccessibilityFocusedView = UNDEFINED; 2688 sendAccessibilityEventForVirtualView(virtualViewId, 2689 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 2690 invalidate(0, 0, mRight, mTopSelectionDividerTop); 2691 return true; 2692 } 2693 } return false; 2694 } 2695 } return false; 2696 } 2697 return super.performAction(virtualViewId, action, arguments); 2698 } 2699 sendAccessibilityEventForVirtualView(int virtualViewId, int eventType)2700 public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) { 2701 switch (virtualViewId) { 2702 case VIRTUAL_VIEW_ID_DECREMENT: { 2703 if (hasVirtualDecrementButton()) { 2704 sendAccessibilityEventForVirtualButton(virtualViewId, eventType, 2705 getVirtualDecrementButtonText()); 2706 } 2707 } break; 2708 case VIRTUAL_VIEW_ID_INPUT: { 2709 sendAccessibilityEventForVirtualText(eventType); 2710 } break; 2711 case VIRTUAL_VIEW_ID_INCREMENT: { 2712 if (hasVirtualIncrementButton()) { 2713 sendAccessibilityEventForVirtualButton(virtualViewId, eventType, 2714 getVirtualIncrementButtonText()); 2715 } 2716 } break; 2717 } 2718 } 2719 sendAccessibilityEventForVirtualText(int eventType)2720 private void sendAccessibilityEventForVirtualText(int eventType) { 2721 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2722 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 2723 mInputText.onInitializeAccessibilityEvent(event); 2724 mInputText.onPopulateAccessibilityEvent(event); 2725 event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); 2726 requestSendAccessibilityEvent(NumberPicker.this, event); 2727 } 2728 } 2729 sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, String text)2730 private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, 2731 String text) { 2732 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2733 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 2734 event.setClassName(Button.class.getName()); 2735 event.setPackageName(mContext.getPackageName()); 2736 event.getText().add(text); 2737 event.setEnabled(NumberPicker.this.isEnabled()); 2738 event.setSource(NumberPicker.this, virtualViewId); 2739 requestSendAccessibilityEvent(NumberPicker.this, event); 2740 } 2741 } 2742 findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, int virtualViewId, List<AccessibilityNodeInfo> outResult)2743 private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, 2744 int virtualViewId, List<AccessibilityNodeInfo> outResult) { 2745 switch (virtualViewId) { 2746 case VIRTUAL_VIEW_ID_DECREMENT: { 2747 String text = getVirtualDecrementButtonText(); 2748 if (!TextUtils.isEmpty(text) 2749 && text.toString().toLowerCase().contains(searchedLowerCase)) { 2750 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT)); 2751 } 2752 } return; 2753 case VIRTUAL_VIEW_ID_INPUT: { 2754 CharSequence text = mInputText.getText(); 2755 if (!TextUtils.isEmpty(text) && 2756 text.toString().toLowerCase().contains(searchedLowerCase)) { 2757 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); 2758 return; 2759 } 2760 CharSequence contentDesc = mInputText.getText(); 2761 if (!TextUtils.isEmpty(contentDesc) && 2762 contentDesc.toString().toLowerCase().contains(searchedLowerCase)) { 2763 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); 2764 return; 2765 } 2766 } break; 2767 case VIRTUAL_VIEW_ID_INCREMENT: { 2768 String text = getVirtualIncrementButtonText(); 2769 if (!TextUtils.isEmpty(text) 2770 && text.toString().toLowerCase().contains(searchedLowerCase)) { 2771 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT)); 2772 } 2773 } return; 2774 } 2775 } 2776 createAccessibiltyNodeInfoForInputText( int left, int top, int right, int bottom)2777 private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText( 2778 int left, int top, int right, int bottom) { 2779 AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo(); 2780 info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); 2781 info.setAccessibilityFocused(mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT); 2782 if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) { 2783 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 2784 } 2785 if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) { 2786 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 2787 } 2788 Rect boundsInParent = mTempRect; 2789 boundsInParent.set(left, top, right, bottom); 2790 info.setVisibleToUser(isVisibleToUser(boundsInParent)); 2791 info.setBoundsInParent(boundsInParent); 2792 Rect boundsInScreen = boundsInParent; 2793 int[] locationOnScreen = mTempArray; 2794 getLocationOnScreen(locationOnScreen); 2795 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); 2796 info.setBoundsInScreen(boundsInScreen); 2797 return info; 2798 } 2799 createAccessibilityNodeInfoForVirtualButton(int virtualViewId, String text, int left, int top, int right, int bottom)2800 private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId, 2801 String text, int left, int top, int right, int bottom) { 2802 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); 2803 info.setClassName(Button.class.getName()); 2804 info.setPackageName(mContext.getPackageName()); 2805 info.setSource(NumberPicker.this, virtualViewId); 2806 info.setParent(NumberPicker.this); 2807 info.setText(text); 2808 info.setClickable(true); 2809 info.setLongClickable(true); 2810 info.setEnabled(NumberPicker.this.isEnabled()); 2811 info.setAccessibilityFocused(mAccessibilityFocusedView == virtualViewId); 2812 Rect boundsInParent = mTempRect; 2813 boundsInParent.set(left, top, right, bottom); 2814 info.setVisibleToUser(isVisibleToUser(boundsInParent)); 2815 info.setBoundsInParent(boundsInParent); 2816 Rect boundsInScreen = boundsInParent; 2817 int[] locationOnScreen = mTempArray; 2818 getLocationOnScreen(locationOnScreen); 2819 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); 2820 info.setBoundsInScreen(boundsInScreen); 2821 2822 if (mAccessibilityFocusedView != virtualViewId) { 2823 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 2824 } 2825 if (mAccessibilityFocusedView == virtualViewId) { 2826 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 2827 } 2828 if (NumberPicker.this.isEnabled()) { 2829 info.addAction(AccessibilityNodeInfo.ACTION_CLICK); 2830 } 2831 2832 return info; 2833 } 2834 createAccessibilityNodeInfoForNumberPicker(int left, int top, int right, int bottom)2835 private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top, 2836 int right, int bottom) { 2837 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); 2838 info.setClassName(NumberPicker.class.getName()); 2839 info.setPackageName(mContext.getPackageName()); 2840 info.setSource(NumberPicker.this); 2841 2842 if (hasVirtualDecrementButton()) { 2843 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT); 2844 } 2845 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); 2846 if (hasVirtualIncrementButton()) { 2847 info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT); 2848 } 2849 2850 info.setParent((View) getParentForAccessibility()); 2851 info.setEnabled(NumberPicker.this.isEnabled()); 2852 info.setScrollable(true); 2853 info.setAccessibilityFocused(mAccessibilityFocusedView == View.NO_ID); 2854 2855 final float applicationScale = 2856 getContext().getResources().getCompatibilityInfo().applicationScale; 2857 2858 Rect boundsInParent = mTempRect; 2859 boundsInParent.set(left, top, right, bottom); 2860 boundsInParent.scale(applicationScale); 2861 info.setBoundsInParent(boundsInParent); 2862 2863 info.setVisibleToUser(isVisibleToUser()); 2864 2865 Rect boundsInScreen = boundsInParent; 2866 int[] locationOnScreen = mTempArray; 2867 getLocationOnScreen(locationOnScreen); 2868 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); 2869 boundsInScreen.scale(applicationScale); 2870 info.setBoundsInScreen(boundsInScreen); 2871 2872 if (mAccessibilityFocusedView != View.NO_ID) { 2873 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 2874 } 2875 if (mAccessibilityFocusedView == View.NO_ID) { 2876 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 2877 } 2878 if (NumberPicker.this.isEnabled()) { 2879 if (getWrapSelectorWheel() || getValue() < getMaxValue()) { 2880 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 2881 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN); 2882 } 2883 if (getWrapSelectorWheel() || getValue() > getMinValue()) { 2884 info.addAction( 2885 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 2886 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); 2887 } 2888 } 2889 2890 return info; 2891 } 2892 hasVirtualDecrementButton()2893 private boolean hasVirtualDecrementButton() { 2894 return getWrapSelectorWheel() || getValue() > getMinValue(); 2895 } 2896 hasVirtualIncrementButton()2897 private boolean hasVirtualIncrementButton() { 2898 return getWrapSelectorWheel() || getValue() < getMaxValue(); 2899 } 2900 getVirtualDecrementButtonText()2901 private String getVirtualDecrementButtonText() { 2902 int value = mValue - 1; 2903 if (mWrapSelectorWheel) { 2904 value = getWrappedSelectorIndex(value); 2905 } 2906 if (value >= mMinValue) { 2907 return (mDisplayedValues == null) ? formatNumber(value) 2908 : mDisplayedValues[value - mMinValue]; 2909 } 2910 return null; 2911 } 2912 getVirtualIncrementButtonText()2913 private String getVirtualIncrementButtonText() { 2914 int value = mValue + 1; 2915 if (mWrapSelectorWheel) { 2916 value = getWrappedSelectorIndex(value); 2917 } 2918 if (value <= mMaxValue) { 2919 return (mDisplayedValues == null) ? formatNumber(value) 2920 : mDisplayedValues[value - mMinValue]; 2921 } 2922 return null; 2923 } 2924 } 2925 formatNumberWithLocale(int value)2926 static private String formatNumberWithLocale(int value) { 2927 return String.format(Locale.getDefault(), "%d", value); 2928 } 2929 } 2930