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