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