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