diff options
| author | Svetoslav Ganov <svetoslavganov@google.com> | 2012-03-20 12:13:02 -0700 |
|---|---|---|
| committer | Svetoslav Ganov <svetoslavganov@google.com> | 2012-03-20 12:13:06 -0700 |
| commit | d11e6151fe88314505fa7adca6278de2e772b11c (patch) | |
| tree | 50945111fae50395bf43905c78abac02751b10c5 /core/java/android/widget/NumberPicker.java | |
| parent | 80a6b33be276eb6c808fce13debb4bb7a489cdad (diff) | |
Revamp of the NumberPicker widget.
1. The number picker no longer shows up and down arrows, it
has only three touch targets which are the currently selected number
in the middle with a lesser one above and greater below, now what
you touch is what you get, flingability and long press are still
supported.
2. Removed the restriction for a View with an AccessibilityNodeProvider
to not have any concrete children. If the View has a provider, then
this provider is responsible for creating the AccessibilityNodeInfos
for all its descendants, concrete and virtual. The number picker is
a good example for such a case - it has a concrete input view and
two virtual buttons as its children. This is a safe change since
this behavior has not been released.
3. This patch also fixes bug where the number picker is stretched too
much in the Theme theme.
bug:6177794
bug:5728294
Change-Id: I5fb370fe0b864a156f5f2aaf2de5f55f6b6d4e84
Diffstat (limited to 'core/java/android/widget/NumberPicker.java')
| -rw-r--r-- | core/java/android/widget/NumberPicker.java | 1299 |
1 files changed, 722 insertions, 577 deletions
diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 3335da0bd672..4e56cd6a46ee 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -16,10 +16,6 @@ package android.widget; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; import android.annotation.Widget; import android.content.Context; import android.content.res.ColorStateList; @@ -48,22 +44,41 @@ import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import com.android.internal.R; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * A widget that enables the user to select a number form a predefined range. - * The widget presents an input field and up and down buttons for selecting the - * current value. Pressing/long-pressing the up and down buttons increments and - * decrements the current value respectively. Touching the input field shows a - * scroll wheel, which when touched allows direct edit - * of the current value. Sliding gestures up or down hide the buttons and the - * input filed, show and rotate the scroll wheel. Flinging is - * also supported. The widget enables mapping from positions to strings such - * that, instead of the position index, the corresponding string is displayed. + * There are two flavors of this widget and which one is presented to the user + * depends on the current theme. + * <ul> + * <li> + * If the current theme is derived from {@link android.R.style#Theme} the widget + * presents the current value as an editable input field with an increment button + * above and a decrement button below. Long pressing the buttons allows for a quick + * change of the current value. Tapping on the input field allows to type in + * a desired value. + * </li> + * <li> + * If the current theme is derived from {@link android.R.style#Theme_Holo} or + * {@link android.R.style#Theme_Holo_Light} the widget presents the current + * value as an editable input field with a lesser value above and a greater + * value below. Tapping on the lesser or greater value selects it by animating + * the number axis up or down to make the chosen value current. Flinging up + * or down allows for multiple increments or decrements of the current value. + * Long pressing on the lesser and greater values also allows for a quick change + * of the current value. Tapping on the current value allows to type in a + * desired value. + * </li> + * </ul> * <p> * For an example of using this widget, see {@link android.widget.TimePicker}. * </p> @@ -74,7 +89,7 @@ public class NumberPicker extends LinearLayout { /** * The number of items show in the selector wheel. */ - public static final int SELECTOR_WHEEL_ITEM_COUNT = 5; + private static final int SELECTOR_WHEEL_ITEM_COUNT = 3; /** * The default update interval during long press. @@ -84,7 +99,7 @@ public class NumberPicker extends LinearLayout { /** * The index of the middle selector item. */ - private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2; + private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2; /** * The coefficient by which to adjust (divide) the max fling velocity. @@ -97,19 +112,12 @@ public class NumberPicker extends LinearLayout { private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; /** - * The duration of scrolling to the next/previous value while changing - * the current value by one, i.e. increment or decrement. + * The duration of scrolling to the next/previous value while changing the + * current value by one, i.e. increment or decrement. */ private static final int CHANGE_CURRENT_BY_ONE_SCROLL_DURATION = 300; /** - * The the delay for showing the input controls after a single tap on the - * input text. - */ - private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration - .getDoubleTapTimeout(); - - /** * The strength of fading in the top and bottom while drawing the selector. */ private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; @@ -120,56 +128,31 @@ public class NumberPicker extends LinearLayout { private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2; /** - * In this state the selector wheel is not shown. - */ - private static final int SELECTOR_WHEEL_STATE_NONE = 0; - - /** - * In this state the selector wheel is small. - */ - private static final int SELECTOR_WHEEL_STATE_SMALL = 1; - - /** - * In this state the selector wheel is large. - */ - private static final int SELECTOR_WHEEL_STATE_LARGE = 2; - - /** - * The alpha of the selector wheel when it is bright. - */ - private static final int SELECTOR_WHEEL_BRIGHT_ALPHA = 255; - - /** - * The alpha of the selector wheel when it is dimmed. + * The default unscaled distance between the selection dividers. */ - private static final int SELECTOR_WHEEL_DIM_ALPHA = 60; + private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48; /** - * The alpha for the increment/decrement button when it is transparent. + * The default unscaled minimal distance for a swipe to be considered a fling. */ - private static final int BUTTON_ALPHA_TRANSPARENT = 0; + private static final int UNSCALED_DEFAULT_MIN_FLING_DISTANCE = 150; /** - * The alpha for the increment/decrement button when it is opaque. + * Coefficient for adjusting touch scroll distance. */ - private static final int BUTTON_ALPHA_OPAQUE = 1; + private static final float TOUCH_SCROLL_DECELERATION_COEFFICIENT = 2.5f; /** - * The property for setting the selector paint. + * The resource id for the default layout. */ - private static final String PROPERTY_SELECTOR_PAINT_ALPHA = "selectorPaintAlpha"; - - /** - * The property for setting the increment/decrement button alpha. - */ - private static final String PROPERTY_BUTTON_ALPHA = "alpha"; + private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker; /** * The numbers accepted by the input text's {@link Filter} */ private static final char[] DIGIT_CHARACTERS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' - }; + }; /** * Constant for unspecified size. @@ -215,6 +198,11 @@ public class NumberPicker extends LinearLayout { private final EditText mInputText; /** + * The distance between the two selection dividers. + */ + private final int mSelectionDividersDistance; + + /** * The min height of this widget. */ private final int mMinHeight; @@ -245,6 +233,11 @@ public class NumberPicker extends LinearLayout { private final int mTextSize; /** + * The minimal distance for a swipe to be considered a fling. + */ + private final int mMinFlingDistance; + + /** * The height of the gap between text elements if the selector wheel. */ private int mSelectorTextGapHeight; @@ -297,10 +290,7 @@ public class NumberPicker extends LinearLayout { /** * The selector indices whose value are show by the selector. */ - private final int[] mSelectorIndices = new int[] { - Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, - Integer.MIN_VALUE - }; + private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT]; /** * The {@link Paint} for drawing the selector. @@ -343,25 +333,15 @@ public class NumberPicker extends LinearLayout { private SetSelectionCommand mSetSelectionCommand; /** - * Handle to the reusable command for adjusting the scroller. - */ - private AdjustScrollerCommand mAdjustScrollerCommand; - - /** * Handle to the reusable command for changing the current value from long * press by one. */ private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand; /** - * {@link Animator} for showing the up/down arrows. - */ - private final AnimatorSet mShowInputControlsAnimator; - - /** - * {@link Animator} for dimming the selector wheel. + * Command for beginning an edit of the current value via IME on long press. */ - private final Animator mDimSelectorWheelAnimator; + private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand; /** * The Y position of the last down event. @@ -369,24 +349,14 @@ public class NumberPicker extends LinearLayout { private float mLastDownEventY; /** - * The Y position of the last motion event. + * The time of the last down event. */ - private float mLastMotionEventY; + private long mLastDownEventTime; /** - * Flag if to check for double tap and potentially start edit. + * The Y position of the last down or move event. */ - private boolean mCheckBeginEditOnUpEvent; - - /** - * Flag if to adjust the selector wheel on next up event. - */ - private boolean mAdjustScrollerOnUpEvent; - - /** - * The state of the selector wheel. - */ - private int mSelectorWheelState; + private float mLastDownOrMoveEventY; /** * Determines speed during touch scrolling. @@ -419,9 +389,9 @@ public class NumberPicker extends LinearLayout { private final int mSolidColor; /** - * Flag indicating if this widget supports flinging. + * Flag whether this widget has a selector wheel. */ - private final boolean mFlingable; + private final boolean mHasSelectorWheel; /** * Divider for showing item to be selected while scrolling @@ -434,29 +404,40 @@ public class NumberPicker extends LinearLayout { private final int mSelectionDividerHeight; /** - * Reusable {@link Rect} instance. + * The current scroll state of the number picker. */ - private final Rect mTempRect = new Rect(); + private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** - * The current scroll state of the number picker. + * Flag whether to ignore move events - we ignore such when we show in IME + * to prevent the content from scrolling. */ - private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; + private boolean mIngonreMoveEvents; /** - * The duration of the animation for showing the input controls. + * Flag whether to show soft input on tap. */ - private final long mShowInputControlsAnimimationDuration; + private boolean mShowSoftInputOnTap; /** - * Flag whether the scoll wheel and the fading edges have been initialized. + * The top of the top selection divider. */ - private boolean mScrollWheelAndFadingEdgesInitialized; + private int mTopSelectionDividerTop; /** - * The time of the last up event. + * The bottom of the bottom selection divider. */ - private long mLastUpEventTimeMillis; + private int mBottomSelectionDividerBottom; + + /** + * The virtual id of the last hovered child. + */ + private int mLastHoveredChildVirtualViewId; + + /** + * Provider to report to clients the semantic structure of this widget. + */ + private AccessibilityNodeProviderImpl mAccessibilityNodeProvider; /** * Interface to listen for changes of the current value. @@ -484,7 +465,7 @@ public class NumberPicker extends LinearLayout { public static int SCROLL_STATE_IDLE = 0; /** - * The user is scrolling using touch, and their finger is still on the screen. + * The user is scrolling using touch, and his finger is still on the screen. */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; @@ -549,58 +530,78 @@ public class NumberPicker extends LinearLayout { super(context, attrs, defStyle); // process style attributes - TypedArray attributesArray = context.obtainStyledAttributes(attrs, - R.styleable.NumberPicker, defStyle, 0); + TypedArray attributesArray = context.obtainStyledAttributes( + attrs, R.styleable.NumberPicker, defStyle, 0); + final int layoutResId = attributesArray.getResourceId( + R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID); + + mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID); + mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); - mFlingable = attributesArray.getBoolean(R.styleable.NumberPicker_flingable, true); + mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider); - int defSelectionDividerHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, + + final int defSelectionDividerHeight = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, getResources().getDisplayMetrics()); mSelectionDividerHeight = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight); + + final int defSelectionDividerDistance = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE, + getResources().getDisplayMetrics()); + mSelectionDividersDistance = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance); + + final int defMinFlingDistance = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_MIN_FLING_DISTANCE, + getResources().getDisplayMetrics()); + mMinFlingDistance = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_minFlingDistance, defMinFlingDistance); + mMinHeight = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED); + mMaxHeight = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED); if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED && mMinHeight > mMaxHeight) { throw new IllegalArgumentException("minHeight > maxHeight"); } - mMinWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_internalMinWidth, - SIZE_UNSPECIFIED); - mMaxWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_internalMaxWidth, - SIZE_UNSPECIFIED); + + mMinWidth = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED); + + mMaxWidth = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED); if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED && mMinWidth > mMaxWidth) { throw new IllegalArgumentException("minWidth > maxWidth"); } + mComputeMaxWidth = (mMaxWidth == Integer.MAX_VALUE); - attributesArray.recycle(); - mShowInputControlsAnimimationDuration = getResources().getInteger( - R.integer.config_longAnimTime); + attributesArray.recycle(); // By default Linearlayout that we extend is not drawn. This is // its draw() method is not called but dispatchDraw() is called // directly (see ViewGroup.drawChild()). However, this class uses // the fading edge effect implemented by View and we need our // draw() method to be called. Therefore, we declare we will draw. - setWillNotDraw(false); - setSelectorWheelState(SELECTOR_WHEEL_STATE_NONE); + setWillNotDraw(!mHasSelectorWheel); LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.number_picker, this, true); + inflater.inflate(layoutResId, this, true); OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { - changeCurrentByOne(true); + changeValueByOne(true); } else { - changeCurrentByOne(false); + changeValueByOne(false); } } }; @@ -610,23 +611,31 @@ public class NumberPicker extends LinearLayout { hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { - postChangeCurrentByOneFromLongPress(true); + postChangeCurrentByOneFromLongPress(true, 0); } else { - postChangeCurrentByOneFromLongPress(false); + postChangeCurrentByOneFromLongPress(false, 0); } return true; } }; // increment button - mIncrementButton = (ImageButton) findViewById(R.id.increment); - mIncrementButton.setOnClickListener(onClickListener); - mIncrementButton.setOnLongClickListener(onLongClickListener); + if (!mHasSelectorWheel) { + mIncrementButton = (ImageButton) findViewById(R.id.increment); + mIncrementButton.setOnClickListener(onClickListener); + mIncrementButton.setOnLongClickListener(onLongClickListener); + } else { + mIncrementButton = null; + } // decrement button - mDecrementButton = (ImageButton) findViewById(R.id.decrement); - mDecrementButton.setOnClickListener(onClickListener); - mDecrementButton.setOnLongClickListener(onLongClickListener); + if (!mHasSelectorWheel) { + mDecrementButton = (ImageButton) findViewById(R.id.decrement); + mDecrementButton.setOnClickListener(onClickListener); + mDecrementButton.setOnLongClickListener(onLongClickListener); + } else { + mDecrementButton = null; + } // input text mInputText = (EditText) findViewById(R.id.numberpicker_input); @@ -648,7 +657,6 @@ public class NumberPicker extends LinearLayout { mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE); // initialize constants - mTouchSlop = ViewConfiguration.getTapTimeout(); ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); @@ -667,69 +675,22 @@ public class NumberPicker extends LinearLayout { paint.setColor(color); mSelectorWheelPaint = paint; - // create the animator for showing the input controls - mDimSelectorWheelAnimator = ObjectAnimator.ofInt(this, PROPERTY_SELECTOR_PAINT_ALPHA, - SELECTOR_WHEEL_BRIGHT_ALPHA, SELECTOR_WHEEL_DIM_ALPHA); - final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton, - PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE); - final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton, - PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE); - mShowInputControlsAnimator = new AnimatorSet(); - mShowInputControlsAnimator.playTogether(mDimSelectorWheelAnimator, showIncrementButton, - showDecrementButton); - mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() { - private boolean mCanceled = false; - - @Override - public void onAnimationEnd(Animator animation) { - if (!mCanceled) { - // if canceled => we still want the wheel drawn - setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL); - } - mCanceled = false; - } - - @Override - public void onAnimationCancel(Animator animation) { - if (mShowInputControlsAnimator.isRunning()) { - mCanceled = true; - } - } - }); - // create the fling and adjust scrollers mFlingScroller = new Scroller(getContext(), null, true); mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); updateInputTextView(); - updateIncrementAndDecrementButtonsVisibilityState(); - - if (mFlingable) { - if (isInEditMode()) { - setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL); - } else { - // Start with shown selector wheel and hidden controls. When made - // visible hide the selector and fade-in the controls to suggest - // fling interaction. - setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); - hideInputControls(); - } - } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mHasSelectorWheel) { + super.onLayout(changed, left, top, right, bottom); + return; + } final int msrdWdth = getMeasuredWidth(); final int msrdHght = getMeasuredHeight(); - // Increment button at the top. - final int inctBtnMsrdWdth = mIncrementButton.getMeasuredWidth(); - final int incrBtnLeft = (msrdWdth - inctBtnMsrdWdth) / 2; - final int incrBtnTop = 0; - final int incrBtnRight = incrBtnLeft + inctBtnMsrdWdth; - final int incrBtnBottom = incrBtnTop + mIncrementButton.getMeasuredHeight(); - mIncrementButton.layout(incrBtnLeft, incrBtnTop, incrBtnRight, incrBtnBottom); - // Input text centered horizontally. final int inptTxtMsrdWdth = mInputText.getMeasuredWidth(); final int inptTxtMsrdHght = mInputText.getMeasuredHeight(); @@ -739,24 +700,23 @@ public class NumberPicker extends LinearLayout { final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); - // Decrement button at the top. - final int decrBtnMsrdWdth = mIncrementButton.getMeasuredWidth(); - final int decrBtnLeft = (msrdWdth - decrBtnMsrdWdth) / 2; - final int decrBtnTop = msrdHght - mDecrementButton.getMeasuredHeight(); - final int decrBtnRight = decrBtnLeft + decrBtnMsrdWdth; - final int decrBtnBottom = msrdHght; - mDecrementButton.layout(decrBtnLeft, decrBtnTop, decrBtnRight, decrBtnBottom); - - if (!mScrollWheelAndFadingEdgesInitialized) { - mScrollWheelAndFadingEdgesInitialized = true; + if (changed) { // need to do all this when we know our size initializeSelectorWheel(); initializeFadingEdges(); + mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2 + - mSelectionDividerHeight; + mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight + + mSelectionDividersDistance; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (!mHasSelectorWheel) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } // Try greedily to fit the max width and height. final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); @@ -769,120 +729,143 @@ public class NumberPicker extends LinearLayout { setMeasuredDimension(widthSize, heightSize); } + /** + * Move to the final position of a scroller. Ensures to force finish the scroller + * and if it is not at its final position a scroll of the selector wheel is + * performed to fast forward to the final position. + * + * @param scroller The scroller to whose final position to get. + * @return True of the a move was performed, i.e. the scroller was not in final position. + */ + private boolean moveToFinalScrollerPosition(Scroller scroller) { + scroller.forceFinished(true); + int amountToScroll = scroller.getFinalY() - scroller.getCurrY(); + int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight; + int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; + if (overshootAdjustment != 0) { + if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) { + if (overshootAdjustment > 0) { + overshootAdjustment -= mSelectorElementHeight; + } else { + overshootAdjustment += mSelectorElementHeight; + } + } + amountToScroll += overshootAdjustment; + scrollBy(0, amountToScroll); + return true; + } + return false; + } + @Override public boolean onInterceptTouchEvent(MotionEvent event) { - if (!isEnabled() || !mFlingable) { + if (!mHasSelectorWheel || !isEnabled()) { return false; } - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mLastMotionEventY = mLastDownEventY = event.getY(); + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { removeAllCallbacks(); - mShowInputControlsAnimator.cancel(); - mDimSelectorWheelAnimator.cancel(); - mCheckBeginEditOnUpEvent = false; - mAdjustScrollerOnUpEvent = true; - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { - mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); - boolean scrollersFinished = mFlingScroller.isFinished() - && mAdjustScroller.isFinished(); - if (!scrollersFinished) { - mFlingScroller.forceFinished(true); - mAdjustScroller.forceFinished(true); - onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - } - mCheckBeginEditOnUpEvent = scrollersFinished; - mAdjustScrollerOnUpEvent = true; + mInputText.setVisibility(View.INVISIBLE); + mLastDownOrMoveEventY = mLastDownEventY = event.getY(); + mLastDownEventTime = event.getEventTime(); + mIngonreMoveEvents = false; + mShowSoftInputOnTap = false; + if (!mFlingScroller.isFinished()) { + mFlingScroller.forceFinished(true); + mAdjustScroller.forceFinished(true); + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } else if (!mAdjustScroller.isFinished()) { + mFlingScroller.forceFinished(true); + mAdjustScroller.forceFinished(true); + } else if (mLastDownEventY < mTopSelectionDividerTop) { hideSoftInput(); - hideInputControls(); - return true; - } - if (isEventInVisibleViewHitRect(event, mIncrementButton) - || isEventInVisibleViewHitRect(event, mDecrementButton)) { - return false; - } - mAdjustScrollerOnUpEvent = false; - setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); - hideSoftInput(); - hideInputControls(); - return true; - case MotionEvent.ACTION_MOVE: - float currentMoveY = event.getY(); - int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); - if (deltaDownY > mTouchSlop) { - mCheckBeginEditOnUpEvent = false; - onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); - setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); + postChangeCurrentByOneFromLongPress( + false, ViewConfiguration.getLongPressTimeout()); + } else if (mLastDownEventY > mBottomSelectionDividerBottom) { hideSoftInput(); - hideInputControls(); - return true; + postChangeCurrentByOneFromLongPress( + true, ViewConfiguration.getLongPressTimeout()); + } else { + mShowSoftInputOnTap = true; + postBeginSoftInputOnLongPressCommand(); } - break; + return true; + } } return false; } @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!isEnabled()) { + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled() || !mHasSelectorWheel) { return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } - mVelocityTracker.addMovement(ev); - int action = ev.getActionMasked(); + mVelocityTracker.addMovement(event); + int action = event.getActionMasked(); switch (action) { - case MotionEvent.ACTION_MOVE: - float currentMoveY = ev.getY(); - if (mCheckBeginEditOnUpEvent - || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + case MotionEvent.ACTION_MOVE: { + if (mIngonreMoveEvents) { + break; + } + float currentMoveY = event.getY(); + if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); if (deltaDownY > mTouchSlop) { - mCheckBeginEditOnUpEvent = false; + removeAllCallbacks(); onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } + } else { + int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY) + / TOUCH_SCROLL_DECELERATION_COEFFICIENT); + scrollBy(0, deltaMoveY); + invalidate(); } - int deltaMoveY = (int) (currentMoveY - mLastMotionEventY); - scrollBy(0, deltaMoveY); - invalidate(); - mLastMotionEventY = currentMoveY; - break; - case MotionEvent.ACTION_UP: - if (mCheckBeginEditOnUpEvent) { - mCheckBeginEditOnUpEvent = false; - final long deltaTapTimeMillis = ev.getEventTime() - mLastUpEventTimeMillis; - if (deltaTapTimeMillis < ViewConfiguration.getDoubleTapTimeout()) { - setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL); - showInputControls(mShowInputControlsAnimimationDuration); - mInputText.requestFocus(); - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - inputMethodManager.showSoftInput(mInputText, 0); - } - mLastUpEventTimeMillis = ev.getEventTime(); - return true; - } - } + mLastDownOrMoveEventY = currentMoveY; + } break; + case MotionEvent.ACTION_UP: { + removeBeginSoftInputCommand(); + removeChangeCurrentByOneFromLongPress(); VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { - fling(initialVelocity); + int deltaMove = (int) (event.getY() - mLastDownEventY); + int absDeltaMoveY = Math.abs(deltaMove); + if (absDeltaMoveY > mMinFlingDistance) { + fling(initialVelocity); + } else { + changeValueByOne(deltaMove < 0); + } onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else { - if (mAdjustScrollerOnUpEvent) { - if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) { - postAdjustScrollerCommand(0); + int eventY = (int) event.getY(); + int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY); + long deltaTime = event.getEventTime() - mLastDownEventTime; + if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) { + if (mShowSoftInputOnTap) { + mShowSoftInputOnTap = false; + showSoftInput(); + } else { + int selectorIndexOffset = (eventY / mSelectorElementHeight) + - SELECTOR_MIDDLE_ITEM_INDEX; + if (selectorIndexOffset > 0) { + changeValueByOne(true); + } else if (selectorIndexOffset < 0) { + changeValueByOne(false); + } } } else { - postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS); + ensureScrollWheelAdjusted(); } + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } mVelocityTracker.recycle(); mVelocityTracker = null; - mLastUpEventTimeMillis = ev.getEventTime(); - break; + } break; } return true; } @@ -891,12 +874,6 @@ public class NumberPicker extends LinearLayout { public boolean dispatchTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { - case MotionEvent.ACTION_MOVE: - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { - removeAllCallbacks(); - forceCompleteChangeCurrentByOneViaScroll(); - } - break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: removeAllCallbacks(); @@ -907,27 +884,75 @@ public class NumberPicker extends LinearLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { - removeAllCallbacks(); + final int keyCode = event.getKeyCode(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + removeAllCallbacks(); + break; } return super.dispatchKeyEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { - removeAllCallbacks(); + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + removeAllCallbacks(); + break; } return super.dispatchTrackballEvent(event); } @Override - public void computeScroll() { - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { - return; + protected boolean dispatchHoverEvent(MotionEvent event) { + if (!mHasSelectorWheel) { + return super.dispatchHoverEvent(event); + } + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + final int eventY = (int) event.getY(); + final int hoveredVirtualViewId; + if (eventY < mTopSelectionDividerTop) { + hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT; + } else if (eventY > mBottomSelectionDividerBottom) { + hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT; + } else { + hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT; + } + final int action = event.getActionMasked(); + AccessibilityNodeProviderImpl provider = + (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider(); + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: { + provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mLastHoveredChildVirtualViewId = hoveredVirtualViewId; + } break; + case MotionEvent.ACTION_HOVER_MOVE: { + if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId + && mLastHoveredChildVirtualViewId != View.NO_ID) { + provider.sendAccessibilityEventForVirtualView( + mLastHoveredChildVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mLastHoveredChildVirtualViewId = hoveredVirtualViewId; + } + } break; + case MotionEvent.ACTION_HOVER_EXIT: { + provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mLastHoveredChildVirtualViewId = View.NO_ID; + } break; + } } + return false; + } + + @Override + public void computeScroll() { Scroller scroller = mFlingScroller; if (scroller.isFinished()) { scroller = mAdjustScroller; @@ -952,16 +977,17 @@ public class NumberPicker extends LinearLayout { @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); - mIncrementButton.setEnabled(enabled); - mDecrementButton.setEnabled(enabled); + if (!mHasSelectorWheel) { + mIncrementButton.setEnabled(enabled); + } + if (!mHasSelectorWheel) { + mDecrementButton.setEnabled(enabled); + } mInputText.setEnabled(enabled); } @Override public void scrollBy(int x, int y) { - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { - return; - } int[] selectorIndices = mSelectorIndices; if (!mWrapSelectorWheel && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { @@ -977,7 +1003,7 @@ public class NumberPicker extends LinearLayout { while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { mCurrentScrollOffset -= mSelectorElementHeight; decrementSelectorIndices(selectorIndices); - changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); + setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; } @@ -985,7 +1011,7 @@ public class NumberPicker extends LinearLayout { while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { mCurrentScrollOffset += mSelectorElementHeight; incrementSelectorIndices(selectorIndices); - changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); + setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; } @@ -1024,8 +1050,7 @@ public class NumberPicker extends LinearLayout { * * @param formatter The formatter object. If formatter is <code>null</code>, * {@link String#valueOf(int)} will be used. - * - * @see #setDisplayedValues(String[]) + *@see #setDisplayedValues(String[]) */ public void setFormatter(Formatter formatter) { if (formatter == mFormatter) { @@ -1068,26 +1093,35 @@ public class NumberPicker extends LinearLayout { if (mValue == value) { return; } - if (value < mMinValue) { - value = mWrapSelectorWheel ? mMaxValue : mMinValue; - } - if (value > mMaxValue) { - value = mWrapSelectorWheel ? mMinValue : mMaxValue; - } - mValue = value; + setValueInternal(value, false); initializeSelectorWheelIndices(); - updateInputTextView(); - updateIncrementAndDecrementButtonsVisibilityState(); invalidate(); } /** - * Hides the soft input of it is active for the input text. + * Shows the soft input for its input text. + */ + private void showSoftInput() { + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (mHasSelectorWheel) { + mInputText.setVisibility(View.VISIBLE); + } + mInputText.requestFocus(); + inputMethodManager.showSoftInput(mInputText, 0); + } + } + + /** + * Hides the soft input if it is active for the input text. */ private void hideSoftInput() { InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + if (mHasSelectorWheel) { + mInputText.setVisibility(View.INVISIBLE); + } } } @@ -1151,23 +1185,22 @@ public class NumberPicker extends LinearLayout { * wrap around the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getMaxValue()} values. * <p> - * By default if the range (max - min) is more than five (the number of - * items shown on the selector wheel) the selector wheel wrapping is - * enabled. + * By default if the range (max - min) is more than the number of items shown + * on the selector wheel the selector wheel wrapping is enabled. * </p> * <p> - * <strong>Note:</strong> If the number of items, i.e. the range - * ({@link #getMaxValue()} - {@link #getMinValue()}) is less than - * {@link #SELECTOR_WHEEL_ITEM_COUNT}, the selector wheel will not - * wrap. Hence, in such a case calling this method is a NOP. + * <strong>Note:</strong> If the number of items, i.e. the range ( + * {@link #getMaxValue()} - {@link #getMinValue()}) is less than + * the number of items shown on the selector wheel, the selector wheel will + * not wrap. Hence, in such a case calling this method is a NOP. * </p> + * * @param wrapSelectorWheel Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelectorWheel) { final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) { mWrapSelectorWheel = wrapSelectorWheel; - updateIncrementAndDecrementButtonsVisibilityState(); } } @@ -1224,6 +1257,7 @@ public class NumberPicker extends LinearLayout { initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); + invalidate(); } /** @@ -1256,6 +1290,7 @@ public class NumberPicker extends LinearLayout { initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); + invalidate(); } /** @@ -1300,102 +1335,49 @@ public class NumberPicker extends LinearLayout { } @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - // make sure we show the controls only the very - // first time the user sees this widget - if (mFlingable && !isInEditMode()) { - // animate a bit slower the very first time - showInputControls(mShowInputControlsAnimimationDuration * 2); - } - } - - @Override protected void onDetachedFromWindow() { removeAllCallbacks(); } @Override - protected void dispatchDraw(Canvas canvas) { - // There is a good reason for doing this. See comments in draw(). - } - - @Override - public void draw(Canvas canvas) { - // Dispatch draw to our children only if we are not currently running - // the animation for simultaneously dimming the scroll wheel and - // showing in the buttons. This class takes advantage of the View - // implementation of fading edges effect to draw the selector wheel. - // However, in View.draw(), the fading is applied after all the children - // have been drawn and we do not want this fading to be applied to the - // buttons. Therefore, we draw our children after we have completed - // drawing ourselves. - super.draw(canvas); - - // Draw our children if we are not showing the selector wheel of fading - // it out - if (mShowInputControlsAnimator.isRunning() - || mSelectorWheelState != SELECTOR_WHEEL_STATE_LARGE) { - long drawTime = getDrawingTime(); - for (int i = 0, count = getChildCount(); i < count; i++) { - View child = getChildAt(i); - if (!child.isShown()) { - continue; - } - drawChild(canvas, getChildAt(i), drawTime); - } - } - } - - @Override protected void onDraw(Canvas canvas) { - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { + if (!mHasSelectorWheel) { + super.onDraw(canvas); return; } - float x = (mRight - mLeft) / 2; float y = mCurrentScrollOffset; - final int restoreCount = canvas.save(); - - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_SMALL) { - Rect clipBounds = canvas.getClipBounds(); - clipBounds.inset(0, mSelectorElementHeight); - canvas.clipRect(clipBounds); - } - // draw the selector wheel int[] selectorIndices = mSelectorIndices; for (int i = 0; i < selectorIndices.length; i++) { int selectorIndex = selectorIndices[i]; String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); - // Do not draw the middle item if input is visible since the input is shown only - // if the wheel is static and it covers the middle item. Otherwise, if the user - // starts editing the text via the IME he may see a dimmed version of the old - // value intermixed with the new one. + // Do not draw the middle item if input is visible since the input + // is shown only if the wheel is static and it covers the middle + // item. Otherwise, if the user starts editing the text via the + // IME he may see a dimmed version of the old value intermixed + // with the new one. if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) { canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); } y += mSelectorElementHeight; } - // draw the selection dividers (only if scrolling and drawable specified) + // draw the selection dividers if (mSelectionDivider != null) { // draw the top divider - int topOfTopDivider = - (getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2; + int topOfTopDivider = mTopSelectionDividerTop; int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); mSelectionDivider.draw(canvas); // draw the bottom divider - int topOfBottomDivider = topOfTopDivider + mSelectorElementHeight; - int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight; + int bottomOfBottomDivider = mBottomSelectionDividerBottom; + int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); mSelectionDivider.draw(canvas); } - - canvas.restoreToCount(restoreCount); } @Override @@ -1408,12 +1390,20 @@ public class NumberPicker extends LinearLayout { public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(NumberPicker.class.getName()); + event.setScrollable(true); + event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); + event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight); } @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(NumberPicker.class.getName()); + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (!mHasSelectorWheel) { + return super.getAccessibilityNodeProvider(); + } + if (mAccessibilityNodeProvider == null) { + mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); + } + return mAccessibilityNodeProvider; } /** @@ -1442,17 +1432,17 @@ public class NumberPicker extends LinearLayout { } /** - * Utility to reconcile a desired size and state, with constraints imposed by - * a MeasureSpec. Tries to respect the min size, unless a different size is - * imposed by the constraints. + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Tries to respect the min size, unless a different size + * is imposed by the constraints. * * @param minSize The minimal desired size. * @param measuredSize The currently measured size. * @param measureSpec The current measure spec. * @return The resolved size and state. */ - private int resolveSizeAndStateRespectingMinSize(int minSize, int measuredSize, - int measureSpec) { + private int resolveSizeAndStateRespectingMinSize( + int minSize, int measuredSize, int measureSpec) { if (minSize != SIZE_UNSPECIFIED) { final int desiredWidth = Math.max(minSize, measuredSize); return resolveSizeAndState(desiredWidth, measureSpec, 0); @@ -1462,8 +1452,8 @@ public class NumberPicker extends LinearLayout { } /** - * Resets the selector indices and clear the cached - * string representation of these indices. + * Resets the selector indices and clear the cached string representation of + * these indices. */ private void initializeSelectorWheelIndices() { mSelectorIndexToStringCache.clear(); @@ -1480,39 +1470,44 @@ public class NumberPicker extends LinearLayout { } /** - * Sets the current value of this NumberPicker, and sets mPrevious to the - * previous value. If current is greater than mEnd less than mStart, the - * value of mCurrent is wrapped around. Subclasses can override this to - * change the wrapping behavior + * Sets the current value of this NumberPicker. * - * @param current the new value of the NumberPicker + * @param current The new value of the NumberPicker. + * @param notifyChange Whether to notify if the current value changed. */ - private void changeCurrent(int current) { + private void setValueInternal(int current, boolean notifyChange) { if (mValue == current) { return; } // Wrap around the values if we go past the start or end if (mWrapSelectorWheel) { current = getWrappedSelectorIndex(current); + } else { + current = Math.max(current, mMinValue); + current = Math.min(current, mMaxValue); } int previous = mValue; - setValue(current); - notifyChange(previous, current); + mValue = current; + updateInputTextView(); + if (notifyChange) { + notifyChange(previous, current); + } } /** * Changes the current value by one which is increment or * decrement based on the passes argument. + * decrement the current value. * * @param increment True to increment, false to decrement. */ - private void changeCurrentByOne(boolean increment) { - if (mFlingable) { - mDimSelectorWheelAnimator.cancel(); + private void changeValueByOne(boolean increment) { + if (mHasSelectorWheel) { mInputText.setVisibility(View.INVISIBLE); - mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); + if (!moveToFinalScrollerPosition(mFlingScroller)) { + moveToFinalScrollerPosition(mAdjustScroller); + } mPreviousScrollerY = 0; - forceCompleteChangeCurrentByOneViaScroll(); if (increment) { mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, CHANGE_CURRENT_BY_ONE_SCROLL_DURATION); @@ -1523,81 +1518,26 @@ public class NumberPicker extends LinearLayout { invalidate(); } else { if (increment) { - changeCurrent(mValue + 1); + setValueInternal(mValue + 1, true); } else { - changeCurrent(mValue - 1); + setValueInternal(mValue - 1, true); } } } - /** - * Ensures that if we are in the process of changing the current value - * by one via scrolling the scroller gets to its final state and the - * value is updated. - */ - private void forceCompleteChangeCurrentByOneViaScroll() { - Scroller scroller = mFlingScroller; - if (!scroller.isFinished()) { - final int yBeforeAbort = scroller.getCurrY(); - scroller.abortAnimation(); - final int yDelta = scroller.getCurrY() - yBeforeAbort; - scrollBy(0, yDelta); - } - } - - /** - * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector - * wheel. - */ - @SuppressWarnings("unused") - // Called via reflection - private void setSelectorPaintAlpha(int alpha) { - mSelectorWheelPaint.setAlpha(alpha); - invalidate(); - } - - /** - * @return If the <code>event</code> is in the visible <code>view</code>. - */ - private boolean isEventInVisibleViewHitRect(MotionEvent event, View view) { - if (view.getVisibility() == VISIBLE) { - view.getHitRect(mTempRect); - return mTempRect.contains((int) event.getX(), (int) event.getY()); - } - return false; - } - - /** - * Sets the <code>selectorWheelState</code>. - */ - private void setSelectorWheelState(int selectorWheelState) { - mSelectorWheelState = selectorWheelState; - if (selectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { - mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); - } - - if (mFlingable && selectorWheelState == SELECTOR_WHEEL_STATE_LARGE - && AccessibilityManager.getInstance(mContext).isEnabled()) { - AccessibilityManager.getInstance(mContext).interrupt(); - String text = mContext.getString(R.string.number_picker_increment_scroll_action); - mInputText.setContentDescription(text); - mInputText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - mInputText.setContentDescription(null); - } - } - private void initializeSelectorWheel() { initializeSelectorWheelIndices(); int[] selectorIndices = mSelectorIndices; int totalTextHeight = selectorIndices.length * mTextSize; float totalTextGapHeight = (mBottom - mTop) - totalTextHeight; - float textGapCount = selectorIndices.length - 1; + float textGapCount = selectorIndices.length; mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f); mSelectorElementHeight = mTextSize + mSelectorTextGapHeight; - // Ensure that the middle item is positioned the same as the text in mInputText + // Ensure that the middle item is positioned the same as the text in + // mInputText int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop(); - mInitialScrollOffset = editTextTextPosition - - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); + mInitialScrollOffset = editTextTextPosition + - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); mCurrentScrollOffset = mInitialScrollOffset; updateInputTextView(); } @@ -1612,16 +1552,14 @@ public class NumberPicker extends LinearLayout { */ private void onScrollerFinished(Scroller scroller) { if (scroller == mFlingScroller) { - if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { - postAdjustScrollerCommand(0); - onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); - } else { + if (!ensureScrollWheelAdjusted()) { updateInputTextView(); - fadeSelectorWheel(mShowInputControlsAnimimationDuration); } + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else { - updateInputTextView(); - showInputControls(mShowInputControlsAnimimationDuration); + if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + updateInputTextView(); + } } } @@ -1654,56 +1592,6 @@ public class NumberPicker extends LinearLayout { } /** - * Hides the input controls which is the up/down arrows and the text field. - */ - private void hideInputControls() { - mShowInputControlsAnimator.cancel(); - mIncrementButton.setVisibility(INVISIBLE); - mDecrementButton.setVisibility(INVISIBLE); - mInputText.setVisibility(INVISIBLE); - } - - /** - * Show the input controls by making them visible and animating the alpha - * property up/down arrows. - * - * @param animationDuration The duration of the animation. - */ - private void showInputControls(long animationDuration) { - updateIncrementAndDecrementButtonsVisibilityState(); - mInputText.setVisibility(VISIBLE); - mShowInputControlsAnimator.setDuration(animationDuration); - mShowInputControlsAnimator.start(); - } - - /** - * Fade the selector wheel via an animation. - * - * @param animationDuration The duration of the animation. - */ - private void fadeSelectorWheel(long animationDuration) { - mInputText.setVisibility(VISIBLE); - mDimSelectorWheelAnimator.setDuration(animationDuration); - mDimSelectorWheelAnimator.start(); - } - - /** - * Updates the visibility state of the increment and decrement buttons. - */ - private void updateIncrementAndDecrementButtonsVisibilityState() { - if (mWrapSelectorWheel || mValue < mMaxValue) { - mIncrementButton.setVisibility(VISIBLE); - } else { - mIncrementButton.setVisibility(INVISIBLE); - } - if (mWrapSelectorWheel || mValue > mMinValue) { - mDecrementButton.setVisibility(VISIBLE); - } else { - mDecrementButton.setVisibility(INVISIBLE); - } - } - - /** * @return The wrapped index <code>selectorIndex</code> value. */ private int getWrappedSelectorIndex(int selectorIndex) { @@ -1749,8 +1637,7 @@ public class NumberPicker extends LinearLayout { /** * Ensures we have a cached string representation of the given <code> - * selectorIndex</code> - * to avoid multiple instantiations of the same string. + * selectorIndex</code> to avoid multiple instantiations of the same string. */ private void ensureCachedScrollSelectorValue(int selectorIndex) { SparseArray<String> cache = mSelectorIndexToStringCache; @@ -1783,7 +1670,7 @@ public class NumberPicker extends LinearLayout { } else { // Check the new value and ensure it's in range int current = getSelectedPos(str.toString()); - changeCurrent(current); + setValueInternal(current, true); } } @@ -1792,25 +1679,23 @@ public class NumberPicker extends LinearLayout { * the string corresponding to the index specified by the current value will * be returned. Otherwise, the formatter specified in {@link #setFormatter} * will be used to format the number. + * + * @return Whether the text was updated. */ - private void updateInputTextView() { + private boolean updateInputTextView() { /* * If we don't have displayed values then use the current number else * find the correct value in the displayed values for the current * number. */ - if (mDisplayedValues == null) { - mInputText.setText(formatNumber(mValue)); - } else { - mInputText.setText(mDisplayedValues[mValue - mMinValue]); + String text = (mDisplayedValues == null) ? formatNumber(mValue) + : mDisplayedValues[mValue - mMinValue]; + if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) { + mInputText.setText(text); + return true; } - mInputText.setSelection(mInputText.getText().length()); - if (mFlingable && AccessibilityManager.getInstance(mContext).isEnabled()) { - String text = mContext.getString(R.string.number_picker_increment_scroll_mode, - mInputText.getText()); - mInputText.setContentDescription(text); - } + return false; } /** @@ -1828,14 +1713,45 @@ public class NumberPicker extends LinearLayout { * * @param increment Whether to increment or decrement the value. */ - private void postChangeCurrentByOneFromLongPress(boolean increment) { - mInputText.clearFocus(); - removeAllCallbacks(); + private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { if (mChangeCurrentByOneFromLongPressCommand == null) { mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); + } else { + removeCallbacks(mChangeCurrentByOneFromLongPressCommand); + } + mChangeCurrentByOneFromLongPressCommand.setStep(increment); + postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis); + } + + /** + * Removes the command for changing the current value by one. + */ + private void removeChangeCurrentByOneFromLongPress() { + if (mChangeCurrentByOneFromLongPressCommand != null) { + removeCallbacks(mChangeCurrentByOneFromLongPressCommand); + } + } + + /** + * Posts a command for beginning an edit of the current value via IME on + * long press. + */ + private void postBeginSoftInputOnLongPressCommand() { + if (mBeginSoftInputOnLongPressCommand == null) { + mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand(); + } else { + removeCallbacks(mBeginSoftInputOnLongPressCommand); + } + postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout()); + } + + /** + * Removes the command for beginning an edit of the current value via IME. + */ + private void removeBeginSoftInputCommand() { + if (mBeginSoftInputOnLongPressCommand != null) { + removeCallbacks(mBeginSoftInputOnLongPressCommand); } - mChangeCurrentByOneFromLongPressCommand.setIncrement(increment); - post(mChangeCurrentByOneFromLongPressCommand); } /** @@ -1845,12 +1761,12 @@ public class NumberPicker extends LinearLayout { if (mChangeCurrentByOneFromLongPressCommand != null) { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } - if (mAdjustScrollerCommand != null) { - removeCallbacks(mAdjustScrollerCommand); - } if (mSetSelectionCommand != null) { removeCallbacks(mSetSelectionCommand); } + if (mBeginSoftInputOnLongPressCommand != null) { + removeCallbacks(mBeginSoftInputOnLongPressCommand); + } } /** @@ -1888,8 +1804,7 @@ public class NumberPicker extends LinearLayout { /** * Posts an {@link SetSelectionCommand} from the given <code>selectionStart - * </code> to - * <code>selectionEnd</code>. + * </code> to <code>selectionEnd</code>. */ private void postSetSelectionCommand(int selectionStart, int selectionEnd) { if (mSetSelectionCommand == null) { @@ -1903,20 +1818,6 @@ public class NumberPicker extends LinearLayout { } /** - * Posts an {@link AdjustScrollerCommand} within the given <code> - * delayMillis</code> - * . - */ - private void postAdjustScrollerCommand(int delayMillis) { - if (mAdjustScrollerCommand == null) { - mAdjustScrollerCommand = new AdjustScrollerCommand(); - } else { - removeCallbacks(mAdjustScrollerCommand); - } - postDelayed(mAdjustScrollerCommand, delayMillis); - } - - /** * Filter for accepting only valid indices or prefixes of the string * representation of valid indices. */ @@ -1934,8 +1835,8 @@ public class NumberPicker extends LinearLayout { } @Override - public CharSequence filter(CharSequence source, int start, int end, Spanned dest, - int dstart, int dend) { + public CharSequence filter( + CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { if (mDisplayedValues == null) { CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); if (filtered == null) { @@ -1981,6 +1882,27 @@ public class NumberPicker extends LinearLayout { } /** + * Ensures that the scroll wheel is adjusted i.e. there is no offset and the + * middle element is in the middle of the widget. + * + * @return Whether an adjustment has been made. + */ + private boolean ensureScrollWheelAdjusted() { + // adjust to the closest value + int deltaY = mInitialScrollOffset - mCurrentScrollOffset; + if (deltaY != 0) { + mPreviousScrollerY = 0; + if (Math.abs(deltaY) > mSelectorElementHeight / 2) { + deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; + } + mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); + invalidate(); + return true; + } + return false; + } + + /** * Command for setting the input text selection. */ class SetSelectionCommand implements Runnable { @@ -1994,39 +1916,18 @@ public class NumberPicker extends LinearLayout { } /** - * Command for adjusting the scroller to show in its center the closest of - * the displayed items. - */ - class AdjustScrollerCommand implements Runnable { - public void run() { - mPreviousScrollerY = 0; - if (mInitialScrollOffset == mCurrentScrollOffset) { - updateInputTextView(); - showInputControls(mShowInputControlsAnimimationDuration); - return; - } - // adjust to the closest value - int deltaY = mInitialScrollOffset - mCurrentScrollOffset; - if (Math.abs(deltaY) > mSelectorElementHeight / 2) { - deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; - } - mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); - invalidate(); - } - } - - /** * Command for changing the current value from a long press by one. */ class ChangeCurrentByOneFromLongPressCommand implements Runnable { private boolean mIncrement; - private void setIncrement(boolean increment) { + private void setStep(boolean increment) { mIncrement = increment; } + @Override public void run() { - changeCurrentByOne(mIncrement); + changeValueByOne(mIncrement); postDelayed(this, mLongPressUpdateInterval); } } @@ -2048,4 +1949,248 @@ public class NumberPicker extends LinearLayout { } } } + + /** + * Command for beginning soft input on long press. + */ + class BeginSoftInputOnLongPressCommand implements Runnable { + + @Override + public void run() { + showSoftInput(); + mIngonreMoveEvents = true; + } + } + + class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider { + private static final int VIRTUAL_VIEW_ID_INCREMENT = 1; + + private static final int VIRTUAL_VIEW_ID_INPUT = 2; + + private static final int VIRTUAL_VIEW_ID_DECREMENT = 3; + + private final Rect mTempRect = new Rect(); + + private final int[] mTempArray = new int[2]; + + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY, + mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); + case VIRTUAL_VIEW_ID_DECREMENT: + return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT, + getVirtualDecrementButtonText(), mScrollX, mScrollY, + mScrollX + (mRight - mLeft), + mTopSelectionDividerTop + mSelectionDividerHeight); + case VIRTUAL_VIEW_ID_INPUT: + return createAccessibiltyNodeInfoForInputText(); + case VIRTUAL_VIEW_ID_INCREMENT: + return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT, + getVirtualIncrementButtonText(), mScrollX, + mBottomSelectionDividerBottom - mSelectionDividerHeight, + mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); + } + return super.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched, + int virtualViewId) { + if (TextUtils.isEmpty(searched)) { + return Collections.emptyList(); + } + String searchedLowerCase = searched.toLowerCase(); + List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>(); + switch (virtualViewId) { + case View.NO_ID: { + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, + VIRTUAL_VIEW_ID_DECREMENT, result); + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, + VIRTUAL_VIEW_ID_INPUT, result); + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, + VIRTUAL_VIEW_ID_INCREMENT, result); + return result; + } + case VIRTUAL_VIEW_ID_DECREMENT: + case VIRTUAL_VIEW_ID_INCREMENT: + case VIRTUAL_VIEW_ID_INPUT: { + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId, + result); + return result; + } + } + return super.findAccessibilityNodeInfosByText(searched, virtualViewId); + } + + @Override + public boolean performAccessibilityAction(int action, int virtualViewId) { + switch (virtualViewId) { + case VIRTUAL_VIEW_ID_INPUT: { + switch (action) { + case AccessibilityNodeInfo.ACTION_FOCUS: { + if (!mInputText.isFocused()) { + return mInputText.requestFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + if (mInputText.isFocused()) { + mInputText.clearFocus(); + return true; + } + } break; + } + } break; + } + return super.performAccessibilityAction(action, virtualViewId); + } + + public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) { + switch (virtualViewId) { + case VIRTUAL_VIEW_ID_DECREMENT: { + sendAccessibilityEventForVirtualButton(virtualViewId, eventType, + getVirtualDecrementButtonText()); + } break; + case VIRTUAL_VIEW_ID_INPUT: { + sendAccessibilityEventForVirtualText(eventType); + } break; + case VIRTUAL_VIEW_ID_INCREMENT: { + sendAccessibilityEventForVirtualButton(virtualViewId, eventType, + getVirtualIncrementButtonText()); + } break; + } + } + + private void sendAccessibilityEventForVirtualText(int eventType) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + mInputText.onInitializeAccessibilityEvent(event); + mInputText.onPopulateAccessibilityEvent(event); + event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); + requestSendAccessibilityEvent(NumberPicker.this, event); + } + + private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, + String text) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setClassName(Button.class.getName()); + event.setPackageName(mContext.getPackageName()); + event.getText().add(text); + event.setEnabled(NumberPicker.this.isEnabled()); + event.setSource(NumberPicker.this, virtualViewId); + requestSendAccessibilityEvent(NumberPicker.this, event); + } + + private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, + int virtualViewId, List<AccessibilityNodeInfo> outResult) { + switch (virtualViewId) { + case VIRTUAL_VIEW_ID_DECREMENT: { + String text = getVirtualDecrementButtonText(); + if (!TextUtils.isEmpty(text) + && text.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT)); + } + } return; + case VIRTUAL_VIEW_ID_INPUT: { + CharSequence text = mInputText.getText(); + if (!TextUtils.isEmpty(text) && + text.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); + return; + } + CharSequence contentDesc = mInputText.getText(); + if (!TextUtils.isEmpty(contentDesc) && + contentDesc.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); + return; + } + } break; + case VIRTUAL_VIEW_ID_INCREMENT: { + String text = getVirtualIncrementButtonText(); + if (!TextUtils.isEmpty(text) + && text.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT)); + } + } return; + } + } + + private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText() { + AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo(); + info.setLongClickable(true); + info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); + return info; + } + + private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId, + String text, int left, int top, int right, int bottom) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setClassName(Button.class.getName()); + info.setPackageName(mContext.getPackageName()); + info.setSource(NumberPicker.this, virtualViewId); + info.setParent(NumberPicker.this); + info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT); + info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); + info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT); + info.setText(text); + info.setClickable(true); + info.setLongClickable(true); + info.setEnabled(NumberPicker.this.isEnabled()); + Rect boundsInParent = mTempRect; + boundsInParent.set(left, top, right, bottom); + info.setBoundsInParent(boundsInParent); + Rect boundsInScreen = boundsInParent; + int[] locationOnScreen = mTempArray; + getLocationOnScreen(locationOnScreen); + boundsInScreen.offsetTo(0, 0); + boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); + info.setBoundsInScreen(boundsInScreen); + return info; + } + + private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top, + int right, int bottom) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setClassName(Button.class.getName()); + info.setPackageName(mContext.getPackageName()); + info.setSource(NumberPicker.this); + info.setParent((View) getParent()); + info.setEnabled(NumberPicker.this.isEnabled()); + info.setScrollable(true); + Rect boundsInParent = mTempRect; + boundsInParent.set(left, top, right, bottom); + info.setBoundsInParent(boundsInParent); + Rect boundsInScreen = boundsInParent; + int[] locationOnScreen = mTempArray; + getLocationOnScreen(locationOnScreen); + boundsInScreen.offsetTo(0, 0); + boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); + info.setBoundsInScreen(boundsInScreen); + return info; + } + + private String getVirtualDecrementButtonText() { + int value = mValue - 1; + if (mWrapSelectorWheel) { + value = getWrappedSelectorIndex(value); + } + if (value >= mMinValue) { + return (mDisplayedValues == null) ? formatNumber(value) + : mDisplayedValues[value - mMinValue]; + } + return null; + } + + private String getVirtualIncrementButtonText() { + int value = mValue + 1; + if (mWrapSelectorWheel) { + value = getWrappedSelectorIndex(value); + } + if (value <= mMaxValue) { + return (mDisplayedValues == null) ? formatNumber(value) + : mDisplayedValues[value - mMinValue]; + } + return null; + } + } } |
