diff options
Diffstat (limited to 'core/java/android/widget/NumberPicker.java')
| -rw-r--r-- | core/java/android/widget/NumberPicker.java | 1300 |
1 files changed, 577 insertions, 723 deletions
diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 506b0c0..3335da0 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -16,6 +16,10 @@ 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; @@ -44,41 +48,22 @@ 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. - * 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> + * 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. * <p> * For an example of using this widget, see {@link android.widget.TimePicker}. * </p> @@ -89,7 +74,7 @@ public class NumberPicker extends LinearLayout { /** * The number of items show in the selector wheel. */ - private static final int SELECTOR_WHEEL_ITEM_COUNT = 3; + public static final int SELECTOR_WHEEL_ITEM_COUNT = 5; /** * The default update interval during long press. @@ -99,7 +84,7 @@ public class NumberPicker extends LinearLayout { /** * The index of the middle selector item. */ - private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2; + private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2; /** * The coefficient by which to adjust (divide) the max fling velocity. @@ -112,12 +97,19 @@ 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; @@ -128,31 +120,56 @@ public class NumberPicker extends LinearLayout { private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2; /** - * The default unscaled distance between the selection dividers. + * 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. */ - private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48; + private static final int SELECTOR_WHEEL_DIM_ALPHA = 60; /** - * The default unscaled minimal distance for a swipe to be considered a fling. + * The alpha for the increment/decrement button when it is transparent. */ - private static final int UNSCALED_DEFAULT_MIN_FLING_DISTANCE = 150; + private static final int BUTTON_ALPHA_TRANSPARENT = 0; /** - * Coefficient for adjusting touch scroll distance. + * The alpha for the increment/decrement button when it is opaque. */ - private static final float TOUCH_SCROLL_DECELERATION_COEFFICIENT = 2.5f; + private static final int BUTTON_ALPHA_OPAQUE = 1; /** - * The resource id for the default layout. + * The property for setting the selector paint. */ - private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker; + 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"; /** * 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. @@ -198,11 +215,6 @@ 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; @@ -233,11 +245,6 @@ 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; @@ -290,7 +297,10 @@ public class NumberPicker extends LinearLayout { /** * The selector indices whose value are show by the selector. */ - private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT]; + private final int[] mSelectorIndices = new int[] { + Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, + Integer.MIN_VALUE + }; /** * The {@link Paint} for drawing the selector. @@ -333,15 +343,25 @@ 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; /** - * Command for beginning an edit of the current value via IME on long press. + * {@link Animator} for showing the up/down arrows. + */ + private final AnimatorSet mShowInputControlsAnimator; + + /** + * {@link Animator} for dimming the selector wheel. */ - private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand; + private final Animator mDimSelectorWheelAnimator; /** * The Y position of the last down event. @@ -349,14 +369,24 @@ public class NumberPicker extends LinearLayout { private float mLastDownEventY; /** - * The time of the last down event. + * The Y position of the last motion event. */ - private long mLastDownEventTime; + private float mLastMotionEventY; /** - * The Y position of the last down or move event. + * Flag if to check for double tap and potentially start edit. */ - private float mLastDownOrMoveEventY; + 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; /** * Determines speed during touch scrolling. @@ -389,9 +419,9 @@ public class NumberPicker extends LinearLayout { private final int mSolidColor; /** - * Flag whether this widget has a selector wheel. + * Flag indicating if this widget supports flinging. */ - private final boolean mHasSelectorWheel; + private final boolean mFlingable; /** * Divider for showing item to be selected while scrolling @@ -404,40 +434,29 @@ public class NumberPicker extends LinearLayout { private final int mSelectionDividerHeight; /** - * The current scroll state of the number picker. - */ - private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - /** - * Flag whether to ignore move events - we ignore such when we show in IME - * to prevent the content from scrolling. - */ - private boolean mIngonreMoveEvents; - - /** - * Flag whether to show soft input on tap. + * Reusable {@link Rect} instance. */ - private boolean mShowSoftInputOnTap; + private final Rect mTempRect = new Rect(); /** - * The top of the top selection divider. + * The current scroll state of the number picker. */ - private int mTopSelectionDividerTop; + private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** - * The bottom of the bottom selection divider. + * The duration of the animation for showing the input controls. */ - private int mBottomSelectionDividerBottom; + private final long mShowInputControlsAnimimationDuration; /** - * The virtual id of the last hovered child. + * Flag whether the scoll wheel and the fading edges have been initialized. */ - private int mLastHoveredChildVirtualViewId; + private boolean mScrollWheelAndFadingEdgesInitialized; /** - * Provider to report to clients the semantic structure of this widget. + * The time of the last up event. */ - private AccessibilityNodeProviderImpl mAccessibilityNodeProvider; + private long mLastUpEventTimeMillis; /** * Interface to listen for changes of the current value. @@ -465,7 +484,7 @@ public class NumberPicker extends LinearLayout { public static int SCROLL_STATE_IDLE = 0; /** - * The user is scrolling using touch, and his finger is still on the screen. + * The user is scrolling using touch, and their finger is still on the screen. */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; @@ -530,78 +549,58 @@ public class NumberPicker extends LinearLayout { super(context, attrs, defStyle); // process style attributes - 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); - + TypedArray attributesArray = context.obtainStyledAttributes(attrs, + R.styleable.NumberPicker, defStyle, 0); mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); - + mFlingable = attributesArray.getBoolean(R.styleable.NumberPicker_flingable, true); mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider); - - final int defSelectionDividerHeight = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, + 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); + // 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(!mHasSelectorWheel); + setWillNotDraw(false); + setSelectorWheelState(SELECTOR_WHEEL_STATE_NONE); LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutResId, this, true); + inflater.inflate(R.layout.number_picker, this, true); OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { - changeValueByOne(true); + changeCurrentByOne(true); } else { - changeValueByOne(false); + changeCurrentByOne(false); } } }; @@ -611,31 +610,23 @@ public class NumberPicker extends LinearLayout { hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { - postChangeCurrentByOneFromLongPress(true, 0); + postChangeCurrentByOneFromLongPress(true); } else { - postChangeCurrentByOneFromLongPress(false, 0); + postChangeCurrentByOneFromLongPress(false); } return true; } }; // increment button - if (!mHasSelectorWheel) { - mIncrementButton = (ImageButton) findViewById(R.id.increment); - mIncrementButton.setOnClickListener(onClickListener); - mIncrementButton.setOnLongClickListener(onLongClickListener); - } else { - mIncrementButton = null; - } + mIncrementButton = (ImageButton) findViewById(R.id.increment); + mIncrementButton.setOnClickListener(onClickListener); + mIncrementButton.setOnLongClickListener(onLongClickListener); // decrement button - if (!mHasSelectorWheel) { - mDecrementButton = (ImageButton) findViewById(R.id.decrement); - mDecrementButton.setOnClickListener(onClickListener); - mDecrementButton.setOnLongClickListener(onLongClickListener); - } else { - mDecrementButton = null; - } + mDecrementButton = (ImageButton) findViewById(R.id.decrement); + mDecrementButton.setOnClickListener(onClickListener); + mDecrementButton.setOnLongClickListener(onLongClickListener); // input text mInputText = (EditText) findViewById(R.id.numberpicker_input); @@ -657,6 +648,7 @@ 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(); @@ -675,22 +667,69 @@ 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(); @@ -700,23 +739,24 @@ public class NumberPicker extends LinearLayout { final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); - if (changed) { + // 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; // 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); @@ -729,143 +769,120 @@ 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 (!mHasSelectorWheel || !isEnabled()) { + if (!isEnabled() || !mFlingable) { return false; } - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mLastMotionEventY = mLastDownEventY = event.getY(); removeAllCallbacks(); - 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(); - postChangeCurrentByOneFromLongPress( - false, ViewConfiguration.getLongPressTimeout()); - } else if (mLastDownEventY > mBottomSelectionDividerBottom) { + 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; hideSoftInput(); - postChangeCurrentByOneFromLongPress( - true, ViewConfiguration.getLongPressTimeout()); - } else { - mShowSoftInputOnTap = true; - postBeginSoftInputOnLongPressCommand(); + 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); + hideSoftInput(); + hideInputControls(); + return true; + } + break; } return false; } @Override - public boolean onTouchEvent(MotionEvent event) { - if (!isEnabled() || !mHasSelectorWheel) { + public boolean onTouchEvent(MotionEvent ev) { + if (!isEnabled()) { return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } - mVelocityTracker.addMovement(event); - int action = event.getActionMasked(); + mVelocityTracker.addMovement(ev); + int action = ev.getActionMasked(); switch (action) { - case MotionEvent.ACTION_MOVE: { - if (mIngonreMoveEvents) { - break; - } - float currentMoveY = event.getY(); - if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + case MotionEvent.ACTION_MOVE: + float currentMoveY = ev.getY(); + if (mCheckBeginEditOnUpEvent + || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); if (deltaDownY > mTouchSlop) { - removeAllCallbacks(); + mCheckBeginEditOnUpEvent = false; onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } - } else { - int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY) - / TOUCH_SCROLL_DECELERATION_COEFFICIENT); - scrollBy(0, deltaMoveY); - invalidate(); } - mLastDownOrMoveEventY = currentMoveY; - } break; - case MotionEvent.ACTION_UP: { - removeBeginSoftInputCommand(); - removeChangeCurrentByOneFromLongPress(); + 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; + } + } VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { - int deltaMove = (int) (event.getY() - mLastDownEventY); - int absDeltaMoveY = Math.abs(deltaMove); - if (absDeltaMoveY > mMinFlingDistance) { - fling(initialVelocity); - } else { - changeValueByOne(deltaMove < 0); - } + fling(initialVelocity); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else { - 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); - } + if (mAdjustScrollerOnUpEvent) { + if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) { + postAdjustScrollerCommand(0); } } else { - ensureScrollWheelAdjusted(); + postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS); } - onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } mVelocityTracker.recycle(); mVelocityTracker = null; - } break; + mLastUpEventTimeMillis = ev.getEventTime(); + break; } return true; } @@ -874,6 +891,12 @@ 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(); @@ -884,75 +907,27 @@ public class NumberPicker extends LinearLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { - final int keyCode = event.getKeyCode(); - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - removeAllCallbacks(); - break; + int keyCode = event.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { + removeAllCallbacks(); } return super.dispatchKeyEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - removeAllCallbacks(); - break; + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + removeAllCallbacks(); } return super.dispatchTrackballEvent(event); } @Override - 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() { + if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { + return; + } Scroller scroller = mFlingScroller; if (scroller.isFinished()) { scroller = mAdjustScroller; @@ -977,17 +952,16 @@ public class NumberPicker extends LinearLayout { @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); - if (!mHasSelectorWheel) { - mIncrementButton.setEnabled(enabled); - } - if (!mHasSelectorWheel) { - mDecrementButton.setEnabled(enabled); - } + mIncrementButton.setEnabled(enabled); + 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) { @@ -1003,7 +977,7 @@ public class NumberPicker extends LinearLayout { while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { mCurrentScrollOffset -= mSelectorElementHeight; decrementSelectorIndices(selectorIndices); - setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); + changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; } @@ -1011,7 +985,7 @@ public class NumberPicker extends LinearLayout { while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { mCurrentScrollOffset += mSelectorElementHeight; incrementSelectorIndices(selectorIndices); - setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); + changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; } @@ -1050,7 +1024,8 @@ 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) { @@ -1093,35 +1068,26 @@ public class NumberPicker extends LinearLayout { if (mValue == value) { return; } - setValueInternal(value, false); + if (value < mMinValue) { + value = mWrapSelectorWheel ? mMaxValue : mMinValue; + } + if (value > mMaxValue) { + value = mWrapSelectorWheel ? mMinValue : mMaxValue; + } + mValue = value; initializeSelectorWheelIndices(); + updateInputTextView(); + updateIncrementAndDecrementButtonsVisibilityState(); invalidate(); } /** - * 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. + * Hides the soft input of 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); - } } } @@ -1185,23 +1151,23 @@ 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 {@link #SELECTOR_WHEEL_ITEM_COUNT} - * (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 five (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 + * {@link #SELECTOR_WHEEL_ITEM_COUNT}, 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(); } } @@ -1258,7 +1224,6 @@ public class NumberPicker extends LinearLayout { initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); - invalidate(); } /** @@ -1291,7 +1256,6 @@ public class NumberPicker extends LinearLayout { initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); - invalidate(); } /** @@ -1336,49 +1300,102 @@ 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 (!mHasSelectorWheel) { - super.onDraw(canvas); + if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) { 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 + // draw the selection dividers (only if scrolling and drawable specified) if (mSelectionDivider != null) { // draw the top divider - int topOfTopDivider = mTopSelectionDividerTop; + int topOfTopDivider = + (getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2; int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); mSelectionDivider.draw(canvas); // draw the bottom divider - int bottomOfBottomDivider = mBottomSelectionDividerBottom; - int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; + int topOfBottomDivider = topOfTopDivider + mSelectorElementHeight; + int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight; mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); mSelectionDivider.draw(canvas); } + + canvas.restoreToCount(restoreCount); } @Override @@ -1391,20 +1408,12 @@ 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 AccessibilityNodeProvider getAccessibilityNodeProvider() { - if (!mHasSelectorWheel) { - return super.getAccessibilityNodeProvider(); - } - if (mAccessibilityNodeProvider == null) { - mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); - } - return mAccessibilityNodeProvider; + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(NumberPicker.class.getName()); } /** @@ -1433,17 +1442,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); @@ -1453,8 +1462,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(); @@ -1471,44 +1480,39 @@ public class NumberPicker extends LinearLayout { } /** - * Sets the current value of this NumberPicker. + * 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 * - * @param current The new value of the NumberPicker. - * @param notifyChange Whether to notify if the current value changed. + * @param current the new value of the NumberPicker */ - private void setValueInternal(int current, boolean notifyChange) { + private void changeCurrent(int current) { 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; - mValue = current; - updateInputTextView(); - if (notifyChange) { - notifyChange(previous, current); - } + setValue(current); + 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 changeValueByOne(boolean increment) { - if (mHasSelectorWheel) { + private void changeCurrentByOne(boolean increment) { + if (mFlingable) { + mDimSelectorWheelAnimator.cancel(); mInputText.setVisibility(View.INVISIBLE); - if (!moveToFinalScrollerPosition(mFlingScroller)) { - moveToFinalScrollerPosition(mAdjustScroller); - } + mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA); mPreviousScrollerY = 0; + forceCompleteChangeCurrentByOneViaScroll(); if (increment) { mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, CHANGE_CURRENT_BY_ONE_SCROLL_DURATION); @@ -1519,26 +1523,81 @@ public class NumberPicker extends LinearLayout { invalidate(); } else { if (increment) { - setValueInternal(mValue + 1, true); + changeCurrent(mValue + 1); } else { - setValueInternal(mValue - 1, true); + changeCurrent(mValue - 1); } } } + /** + * 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; + float textGapCount = selectorIndices.length - 1; 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(); } @@ -1553,14 +1612,16 @@ public class NumberPicker extends LinearLayout { */ private void onScrollerFinished(Scroller scroller) { if (scroller == mFlingScroller) { - if (!ensureScrollWheelAdjusted()) { + if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) { + postAdjustScrollerCommand(0); + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } else { updateInputTextView(); + fadeSelectorWheel(mShowInputControlsAnimimationDuration); } - onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else { - if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { - updateInputTextView(); - } + updateInputTextView(); + showInputControls(mShowInputControlsAnimimationDuration); } } @@ -1593,6 +1654,56 @@ 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) { @@ -1638,7 +1749,8 @@ 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; @@ -1671,7 +1783,7 @@ public class NumberPicker extends LinearLayout { } else { // Check the new value and ensure it's in range int current = getSelectedPos(str.toString()); - setValueInternal(current, true); + changeCurrent(current); } } @@ -1680,23 +1792,25 @@ 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 boolean updateInputTextView() { + private void 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. */ - String text = (mDisplayedValues == null) ? formatNumber(mValue) - : mDisplayedValues[mValue - mMinValue]; - if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) { - mInputText.setText(text); - return true; + if (mDisplayedValues == null) { + mInputText.setText(formatNumber(mValue)); + } else { + mInputText.setText(mDisplayedValues[mValue - mMinValue]); } + mInputText.setSelection(mInputText.getText().length()); - return false; + if (mFlingable && AccessibilityManager.getInstance(mContext).isEnabled()) { + String text = mContext.getString(R.string.number_picker_increment_scroll_mode, + mInputText.getText()); + mInputText.setContentDescription(text); + } } /** @@ -1714,45 +1828,14 @@ public class NumberPicker extends LinearLayout { * * @param increment Whether to increment or decrement the value. */ - private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { + private void postChangeCurrentByOneFromLongPress(boolean increment) { + mInputText.clearFocus(); + removeAllCallbacks(); 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); } /** @@ -1762,12 +1845,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); - } } /** @@ -1805,7 +1888,8 @@ 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) { @@ -1819,6 +1903,20 @@ 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. */ @@ -1836,8 +1934,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) { @@ -1883,27 +1981,6 @@ 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 { @@ -1917,18 +1994,39 @@ 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 setStep(boolean increment) { + private void setIncrement(boolean increment) { mIncrement = increment; } - @Override public void run() { - changeValueByOne(mIncrement); + changeCurrentByOne(mIncrement); postDelayed(this, mLongPressUpdateInterval); } } @@ -1950,248 +2048,4 @@ 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; - } - } } |
