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