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