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