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