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