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