/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import com.android.internal.R; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.Widget; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; import android.text.InputFilter; import android.text.InputType; import android.text.Spanned; import android.text.TextUtils; import android.text.method.NumberKeyListener; import android.util.AttributeSet; import android.util.SparseArray; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.OvershootInterpolator; import android.view.inputmethod.InputMethodManager; /** * A widget that enables the user to select a number form a predefined range. * The widget presents an input filed 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 filed shows a * scroll wheel, tapping on which while shown and not moving allows direct edit * of the current value. Sliding motions up or down hide the buttons and the * input filed, show the scroll wheel, and rotate the latter. Flinging is * also supported. The widget enables mapping from positions to strings such * that instead the position index the corresponding string is displayed. *

* For an example of using this widget, see {@link android.widget.TimePicker}. *

*/ @Widget public class NumberPicker extends LinearLayout { /** * The index of the middle selector item. */ private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2; /** * The coefficient by which to adjust (divide) the max fling velocity. */ private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; /** * The the duration for adjusting the selector wheel. */ private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; /** * 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 update step for incrementing the current value. */ private static final int UPDATE_STEP_INCREMENT = 1; /** * The update step for decrementing the current value. */ private static final int UPDATE_STEP_DECREMENT = -1; /** * 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; /** * 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' }; /** * Use a custom NumberPicker formatting callback to use two-digit minutes * strings like "01". Keeping a static formatter etc. is the most efficient * way to do this; it avoids creating temporary objects on every call to * format(). */ public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() { final StringBuilder mBuilder = new StringBuilder(); final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US); final Object[] mArgs = new Object[1]; public String toString(int value) { mArgs[0] = value; mBuilder.delete(0, mBuilder.length()); mFmt.format("%02d", mArgs); return mFmt.toString(); } }; /** * The increment button. */ private final ImageButton mIncrementButton; /** * The decrement button. */ private final ImageButton mDecrementButton; /** * The text for showing the current value. */ private final EditText mInputText; /** * The height of the text. */ private final int mTextSize; /** * The values to be displayed instead the indices. */ private String[] mDisplayedValues; /** * Lower value of the range of numbers allowed for the NumberPicker */ private int mStart; /** * Upper value of the range of numbers allowed for the NumberPicker */ private int mEnd; /** * Current value of this NumberPicker */ private int mCurrent; /** * Listener to be notified upon current value change. */ private OnChangeListener mOnChangeListener; /** * Listener to be notified upon scroll state change. */ private OnScrollListener mOnScrollListener; /** * Formatter for for displaying the current value. */ private Formatter mFormatter; /** * The speed for updating the value form long press. */ private long mLongPressUpdateInterval = 300; /** * Cache for the string representation of selector indices. */ private final SparseArray mSelectorIndexToStringCache = new SparseArray(); /** * 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 }; /** * The {@link Paint} for drawing the selector. */ private final Paint mSelectorPaint; /** * The height of a selector element (text + gap). */ private int mSelectorElementHeight; /** * The initial offset of the scroll selector. */ private int mInitialScrollOffset = Integer.MIN_VALUE; /** * The current offset of the scroll selector. */ private int mCurrentScrollOffset; /** * The {@link Scroller} responsible for flinging the selector. */ private final Scroller mFlingScroller; /** * The {@link Scroller} responsible for adjusting the selector. */ private final Scroller mAdjustScroller; /** * The previous Y coordinate while scrolling the selector. */ private int mPreviousScrollerY; /** * Handle to the reusable command for setting the input text selection. */ private SetSelectionCommand mSetSelectionCommand; /** * Handle to the reusable command for adjusting the scroller. */ private AdjustScrollerCommand mAdjustScrollerCommand; /** * Handle to the reusable command for updating the current value from long * press. */ private UpdateValueFromLongPressCommand mUpdateFromLongPressCommand; /** * {@link Animator} for showing the up/down arrows. */ private final AnimatorSet mShowInputControlsAnimator; /** * The Y position of the last down event. */ private float mLastDownEventY; /** * The Y position of the last motion event. */ private float mLastMotionEventY; /** * Flag if to begin edit on next up event. */ private boolean mBeginEditOnUpEvent; /** * Flag if to adjust the selector wheel on next up event. */ private boolean mAdjustScrollerOnUpEvent; /** * Flag if to draw the selector wheel. */ private boolean mDrawSelectorWheel; /** * Determines speed during touch scrolling. */ private VelocityTracker mVelocityTracker; /** * @see ViewConfiguration#getScaledTouchSlop() */ private int mTouchSlop; /** * @see ViewConfiguration#getScaledMinimumFlingVelocity() */ private int mMinimumFlingVelocity; /** * @see ViewConfiguration#getScaledMaximumFlingVelocity() */ private int mMaximumFlingVelocity; /** * Flag whether the selector should wrap around. */ private boolean mWrapSelector; /** * The back ground color used to optimize scroller fading. */ private final int mSolidColor; /** * Reusable {@link Rect} instance. */ private final Rect mTempRect = new Rect(); /** * The current scroll state of the number picker. */ private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** * The callback interface used to indicate the number value has changed. */ public interface OnChangeListener { /** * Called upon a change of the current value. * * @param picker The NumberPicker associated with this listener. * @param oldVal The previous value. * @param newVal The new value. */ void onChange(NumberPicker picker, int oldVal, int newVal); } /** * Interface for listening to the picker scroll state. */ public interface OnScrollListener { /** * The view is not scrolling. */ public static int SCROLL_STATE_IDLE = 0; /** * The user is scrolling using touch, and their finger is still on the screen. */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; /** * The user had previously been scrolling using touch and performed a fling. */ public static int SCROLL_STATE_FLING = 2; /** * Callback method to be invoked while the number picker is being scrolled. * * @param view The view whose scroll state is being reported * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. */ public void onScrollStateChange(NumberPicker view, int scrollState); } /** * Interface used to format the number into a string for presentation. */ public interface Formatter { /** * Formats a string representation of the current index. * * @param value The currently selected value. * @return A formatted string representation. */ public String toString(int value); } /** * Create a new number picker. * * @param context The application environment. */ public NumberPicker(Context context) { this(context, null); } /** * Create a new number picker. * * @param context The application environment. * @param attrs A collection of attributes. */ public NumberPicker(Context context, AttributeSet attrs) { this(context, attrs, R.attr.numberPickerStyle); } /** * Create a new number picker * * @param context the application environment. * @param attrs a collection of attributes. * @param defStyle The default style to apply to this view. */ public NumberPicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // process style attributes TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.NumberPicker, defStyle, 0); int orientation = attributesArray.getInt(R.styleable.NumberPicker_orientation, VERTICAL); setOrientation(orientation); mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); 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); setDrawSelectorWheel(false); LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.number_picker, this, true); OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { mInputText.clearFocus(); if (v.getId() == R.id.increment) { changeCurrent(mCurrent + 1); } else { changeCurrent(mCurrent - 1); } } }; OnLongClickListener onLongClickListener = new OnLongClickListener() { public boolean onLongClick(View v) { mInputText.clearFocus(); if (v.getId() == R.id.increment) { postUpdateValueFromLongPress(UPDATE_STEP_INCREMENT); } else { postUpdateValueFromLongPress(UPDATE_STEP_DECREMENT); } return true; } }; // increment button mIncrementButton = (ImageButton) findViewById(R.id.increment); mIncrementButton.setOnClickListener(onClickListener); mIncrementButton.setOnLongClickListener(onLongClickListener); // decrement button mDecrementButton = (ImageButton) findViewById(R.id.decrement); mDecrementButton.setOnClickListener(onClickListener); mDecrementButton.setOnLongClickListener(onLongClickListener); // input text mInputText = (EditText) findViewById(R.id.timepicker_input); mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { validateInputTextView(v); } } }); mInputText.setFilters(new InputFilter[] { new InputTextFilter() }); mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); // initialize constants mTouchSlop = ViewConfiguration.getTapTimeout(); ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; mTextSize = (int) mInputText.getTextSize(); // create the selector wheel paint Paint paint = new Paint(); paint.setAntiAlias(true); paint.setTextAlign(Align.CENTER); paint.setTextSize(mTextSize); paint.setTypeface(mInputText.getTypeface()); ColorStateList colors = mInputText.getTextColors(); int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); paint.setColor(color); mSelectorPaint = paint; // create the animator for showing the input controls final ValueAnimator fadeScroller = ObjectAnimator.ofInt(this, "selectorPaintAlpha", 255, 0); final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton, "alpha", 0, 1); final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton, "alpha", 0, 1); mShowInputControlsAnimator = new AnimatorSet(); mShowInputControlsAnimator.playTogether(fadeScroller, showIncrementButton, showDecrementButton); mShowInputControlsAnimator.setDuration(getResources().getInteger( R.integer.config_longAnimTime)); mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() { private boolean mCanceled = false; @Override public void onAnimationEnd(Animator animation) { if (!mCanceled) { // if canceled => we still want the wheel drawn setDrawSelectorWheel(false); } mCanceled = false; mSelectorPaint.setAlpha(255); invalidate(); } @Override public void onAnimationCancel(Animator animation) { if (mShowInputControlsAnimator.isRunning()) { mCanceled = true; } } }); // create the fling and adjust scrollers mFlingScroller = new Scroller(getContext()); mAdjustScroller = new Scroller(getContext(), new OvershootInterpolator()); updateInputTextView(); updateIncrementAndDecrementButtonsVisibilityState(); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (!hasWindowFocus) { removeAllCallbacks(); } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mLastMotionEventY = mLastDownEventY = event.getY(); removeAllCallbacks(); mBeginEditOnUpEvent = false; mAdjustScrollerOnUpEvent = true; if (mDrawSelectorWheel) { boolean scrollersFinished = mFlingScroller.isFinished() && mAdjustScroller.isFinished(); if (!scrollersFinished) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE); } mBeginEditOnUpEvent = scrollersFinished; mAdjustScrollerOnUpEvent = true; hideInputControls(); return true; } if (isEventInInputText(event)) { mAdjustScrollerOnUpEvent = false; setDrawSelectorWheel(true); hideInputControls(); return true; } break; case MotionEvent.ACTION_MOVE: float currentMoveY = event.getY(); int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); if (deltaDownY > mTouchSlop) { mBeginEditOnUpEvent = false; tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); setDrawSelectorWheel(true); hideInputControls(); return true; } break; } return false; } @Override public boolean onTouchEvent(MotionEvent ev) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); int action = ev.getActionMasked(); switch (action) { case MotionEvent.ACTION_MOVE: float currentMoveY = ev.getY(); if (mBeginEditOnUpEvent || mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); if (deltaDownY > mTouchSlop) { mBeginEditOnUpEvent = false; tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } int deltaMoveY = (int) (currentMoveY - mLastMotionEventY); scrollBy(0, deltaMoveY); invalidate(); mLastMotionEventY = currentMoveY; break; case MotionEvent.ACTION_UP: if (mBeginEditOnUpEvent) { setDrawSelectorWheel(false); showInputControls(); mInputText.requestFocus(); InputMethodManager imm = (InputMethodManager) getContext().getSystemService( Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mInputText, 0); mInputText.setSelection(0, mInputText.getText().length()); return true; } VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { fling(initialVelocity); tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_FLING); } else { if (mAdjustScrollerOnUpEvent) { if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) { postAdjustScrollerCommand(0); } } else { postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS); } } mVelocityTracker.recycle(); mVelocityTracker = null; break; } return true; } @Override public boolean dispatchTouchEvent(MotionEvent event) { int action = event.getActionMasked(); if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) && !isEventInInputText(event)) { removeAllCallbacks(); } return super.dispatchTouchEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { 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) { int action = event.getActionMasked(); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { removeAllCallbacks(); } return super.dispatchTrackballEvent(event); } @Override public void computeScroll() { if (!mDrawSelectorWheel) { return; } Scroller scroller = mFlingScroller; if (scroller.isFinished()) { scroller = mAdjustScroller; if (scroller.isFinished()) { return; } } scroller.computeScrollOffset(); int currentScrollerY = scroller.getCurrY(); if (mPreviousScrollerY == 0) { mPreviousScrollerY = scroller.getStartY(); } scrollBy(0, currentScrollerY - mPreviousScrollerY); mPreviousScrollerY = currentScrollerY; if (scroller.isFinished()) { onScrollerFinished(scroller); } else { invalidate(); } } /** * Set the enabled state of this view. The interpretation of the enabled * state varies by subclass. * * @param enabled True if this view is enabled, false otherwise. */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); mIncrementButton.setEnabled(enabled); mDecrementButton.setEnabled(enabled); mInputText.setEnabled(enabled); } /** * Scrolls the selector with the given vertical offset. */ @Override public void scrollBy(int x, int y) { int[] selectorIndices = getSelectorIndices(); if (mInitialScrollOffset == Integer.MIN_VALUE) { int totalTextHeight = selectorIndices.length * mTextSize; int totalTextGapHeight = (mBottom - mTop) - totalTextHeight; int textGapCount = selectorIndices.length - 1; int selectorTextGapHeight = totalTextGapHeight / textGapCount; // compensate for integer division loss of the components used to // calculate the text gap int integerDivisionLoss = (mTextSize + mBottom - mTop) % textGapCount; mInitialScrollOffset = mCurrentScrollOffset = mTextSize - integerDivisionLoss / 2; mSelectorElementHeight = mTextSize + selectorTextGapHeight; } if (!mWrapSelector && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) { mCurrentScrollOffset = mInitialScrollOffset; return; } if (!mWrapSelector && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) { mCurrentScrollOffset = mInitialScrollOffset; return; } mCurrentScrollOffset += y; while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight) { mCurrentScrollOffset -= mSelectorElementHeight; decrementSelectorIndices(selectorIndices); changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) { mCurrentScrollOffset = mInitialScrollOffset; } } while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight) { mCurrentScrollOffset += mSelectorElementHeight; incrementScrollSelectorIndices(selectorIndices); changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) { mCurrentScrollOffset = mInitialScrollOffset; } } } @Override public int getSolidColor() { return mSolidColor; } /** * Sets the listener to be notified on change of the current value. * * @param onChangeListener The listener. */ public void setOnChangeListener(OnChangeListener onChangeListener) { mOnChangeListener = onChangeListener; } /** * Set listener to be notified for scroll state changes. * * @param onScrollListener the callback, should not be null. */ public void setOnScrollListener(OnScrollListener onScrollListener) { mOnScrollListener = onScrollListener; } /** * Set the formatter to be used for formatting the current value. *

* Note: If you have provided alternative values for the selected positons * this formatter is never invoked. *

* * @param formatter the formatter object. If formatter is null, * String.valueOf() will be used. * * @see #setRange(int, int, String[]) */ public void setFormatter(Formatter formatter) { mFormatter = formatter; } /** * Set the range of numbers allowed for the number picker. The current value * will be automatically set to the start. * * @param start the start of the range (inclusive) * @param end the end of the range (inclusive) */ public void setRange(int start, int end) { setRange(start, end, null); } /** * Set the range of numbers allowed for the number picker. The current value * will be automatically set to the start. Also provide a mapping for values * used to display to the user instead of the numbers in the range. * * @param start The start of the range (inclusive). * @param end The end of the range (inclusive). * @param displayedValues The values displayed to the user. */ public void setRange(int start, int end, String[] displayedValues) { boolean wrapSelector = (end - start) >= mSelectorIndices.length; setRange(start, end, displayedValues, wrapSelector); } /** * Set the range of numbers allowed for the number picker. The current value * will be automatically set to the start. Also provide a mapping for values * used to display to the user. *

* Note: The wrapSelectorWheel argument is ignored if the range * (difference between start and end) us less than * five since this is the number of values shown by the selector wheel. *

* * @param start the start of the range (inclusive) * @param end the end of the range (inclusive) * @param displayedValues the values displayed to the user. * @param wrapSelectorWheel Whether to wrap the selector wheel. * * @see #setWrapSelectorWheel(boolean) */ public void setRange(int start, int end, String[] displayedValues, boolean wrapSelectorWheel) { if (start < 0 || end < 0) { throw new IllegalArgumentException("start and end must be > 0"); } mDisplayedValues = displayedValues; mStart = start; mEnd = end; mCurrent = start; setWrapSelectorWheel(wrapSelectorWheel); updateInputTextView(); if (displayedValues != null) { // Allow text entry rather than strictly numeric entry. mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } else { mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); } // make sure cached string representations are dropped mSelectorIndexToStringCache.clear(); } /** * Set the current value for the number picker. * * @param current the current value the start of the range (inclusive) * * @throws IllegalArgumentException when current is not within the range of * of the number picker. */ public void setCurrent(int current) { if (current < mStart || current > mEnd) { throw new IllegalArgumentException("current should be >= start and <= end"); } mCurrent = current; updateInputTextView(); updateIncrementAndDecrementButtonsVisibilityState(); } /** * Sets whether the selector wheel shown during flinging/scrolling should wrap * around the beginning and end values. By default if the range is more than * five (the number of items shown on the selector wheel) the selector wheel * wrapping is enabled. * * @param wrapSelector Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelector) { if (wrapSelector && (mEnd - mStart) < mSelectorIndices.length) { throw new IllegalStateException("Range less than selector items count."); } if (wrapSelector != mWrapSelector) { // force the selector indices array to be reinitialized mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE; mWrapSelector = wrapSelector; } } /** * Sets the speed at which the numbers be incremented and decremented when * the up and down buttons are long pressed respectively. * * @param intervalMillis The speed (in milliseconds) at which the numbers * will be incremented and decremented (default 300ms). */ public void setOnLongPressUpdateInterval(long intervalMillis) { mLongPressUpdateInterval = intervalMillis; } /** * Returns the current value of the NumberPicker. * * @return the current value. */ public int getCurrent() { return mCurrent; } /** * Returns the range lower value of the NumberPicker. * * @return The lower number of the range. */ public int getRangeStart() { return mStart; } /** * Returns the range end value of the NumberPicker. * * @return The upper number of the range. */ public int getRangeEnd() { return mEnd; } @Override protected float getTopFadingEdgeStrength() { return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; } @Override protected float getBottomFadingEdgeStrength() { return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; } @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 fading out 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 which are currently showing in. Therefore, we draw our // children // after we have completed drawing ourselves. // Draw the selector wheel if needed if (mDrawSelectorWheel) { super.draw(canvas); } // Draw our children if we are not showing the selector wheel of fading // it out if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) { 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) { // we only draw the selector wheel if (!mDrawSelectorWheel) { return; } float x = (mRight - mLeft) / 2; float y = mCurrentScrollOffset; int[] selectorIndices = getSelectorIndices(); for (int i = 0; i < selectorIndices.length; i++) { int selectorIndex = selectorIndices[i]; String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint); y += mSelectorElementHeight; } } /** * 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 */ private void changeCurrent(int current) { if (mCurrent == current) { return; } // Wrap around the values if we go past the start or end if (mWrapSelector) { current = getWrappedSelectorIndex(current); } int previous = mCurrent; setCurrent(current); notifyChange(previous, current); } /** * Sets the alpha of the {@link Paint} for drawing the selector * wheel. */ @SuppressWarnings("unused") // Called by ShowInputControlsAnimator via reflection private void setSelectorPaintAlpha(int alpha) { mSelectorPaint.setAlpha(alpha); if (mDrawSelectorWheel) { invalidate(); } } /** * @return If the event is in the input text. */ private boolean isEventInInputText(MotionEvent event) { mInputText.getHitRect(mTempRect); return mTempRect.contains((int) event.getX(), (int) event.getY()); } /** * Sets if to drawSelectionWheel. */ private void setDrawSelectorWheel(boolean drawSelectorWheel) { mDrawSelectorWheel = drawSelectorWheel; // do not fade if the selector wheel not shown setVerticalFadingEdgeEnabled(drawSelectorWheel); } /** * Callback invoked upon completion of a given scroller. */ private void onScrollerFinished(Scroller scroller) { if (scroller == mFlingScroller) { postAdjustScrollerCommand(0); tryNotifyScrollListener(OnScrollListener.SCROLL_STATE_IDLE); } else { showInputControls(); updateInputTextView(); } } /** * Notifies the scroll listener for the given scrollState * if the scroll state differs from the current scroll state. */ private void tryNotifyScrollListener(int scrollState) { if (mOnScrollListener != null && mScrollState != scrollState) { mScrollState = scrollState; mOnScrollListener.onScrollStateChange(this, scrollState); } } /** * Flings the selector with the given velocityY. */ private void fling(int velocityY) { mPreviousScrollerY = 0; Scroller flingScroller = mFlingScroller; if (mWrapSelector) { if (velocityY > 0) { flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); } else { flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); } } else { if (velocityY > 0) { int maxY = mTextSize * (mCurrent - mStart); flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY); } else { int startY = mTextSize * (mEnd - mCurrent); int maxY = startY; flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY); } } postAdjustScrollerCommand(flingScroller.getDuration()); invalidate(); } /** * 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. */ private void showInputControls() { updateIncrementAndDecrementButtonsVisibilityState(); mInputText.setVisibility(VISIBLE); mShowInputControlsAnimator.start(); } /** * Updates the visibility state of the increment and decrement buttons. */ private void updateIncrementAndDecrementButtonsVisibilityState() { if (mWrapSelector || mCurrent < mEnd) { mIncrementButton.setVisibility(VISIBLE); } else { mIncrementButton.setVisibility(INVISIBLE); } if (mWrapSelector || mCurrent > mStart) { mDecrementButton.setVisibility(VISIBLE); } else { mDecrementButton.setVisibility(INVISIBLE); } } /** * @return The selector indices array with proper values with the current as * the middle one. */ private int[] getSelectorIndices() { int current = getCurrent(); if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) { for (int i = 0; i < mSelectorIndices.length; i++) { int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); if (mWrapSelector) { selectorIndex = getWrappedSelectorIndex(selectorIndex); } mSelectorIndices[i] = selectorIndex; ensureCachedScrollSelectorValue(mSelectorIndices[i]); } } return mSelectorIndices; } /** * @return The wrapped index selectorIndex value. *

* Note: The absolute value of the argument is never larger than * mEnd - mStart. *

*/ private int getWrappedSelectorIndex(int selectorIndex) { if (selectorIndex > mEnd) { return (Math.abs(selectorIndex) - mEnd); } else if (selectorIndex < mStart) { return (mEnd - Math.abs(selectorIndex)); } return selectorIndex; } /** * Increments the selectorIndices whose string representations * will be displayed in the selector. */ private void incrementScrollSelectorIndices(int[] selectorIndices) { for (int i = 0; i < selectorIndices.length - 1; i++) { selectorIndices[i] = selectorIndices[i + 1]; } int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; if (mWrapSelector && nextScrollSelectorIndex > mEnd) { nextScrollSelectorIndex = mStart; } selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; ensureCachedScrollSelectorValue(nextScrollSelectorIndex); } /** * Decrements the selectorIndices whose string representations * will be displayed in the selector. */ private void decrementSelectorIndices(int[] selectorIndices) { for (int i = selectorIndices.length - 1; i > 0; i--) { selectorIndices[i] = selectorIndices[i - 1]; } int nextScrollSelectorIndex = selectorIndices[1] - 1; if (mWrapSelector && nextScrollSelectorIndex < mStart) { nextScrollSelectorIndex = mEnd; } selectorIndices[0] = nextScrollSelectorIndex; ensureCachedScrollSelectorValue(nextScrollSelectorIndex); } /** * Ensures we have a cached string representation of the given * selectorIndex * to avoid multiple instantiations of the same string. */ private void ensureCachedScrollSelectorValue(int selectorIndex) { SparseArray cache = mSelectorIndexToStringCache; String scrollSelectorValue = cache.get(selectorIndex); if (scrollSelectorValue != null) { return; } if (selectorIndex < mStart || selectorIndex > mEnd) { scrollSelectorValue = ""; } else { if (mDisplayedValues != null) { int displayedValueIndex = selectorIndex - mStart; scrollSelectorValue = mDisplayedValues[displayedValueIndex]; } else { scrollSelectorValue = formatNumber(selectorIndex); } } cache.put(selectorIndex, scrollSelectorValue); } private String formatNumber(int value) { return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value); } private void validateInputTextView(View v) { String str = String.valueOf(((TextView) v).getText()); if (TextUtils.isEmpty(str)) { // Restore to the old value as we don't allow empty values updateInputTextView(); } else { // Check the new value and ensure it's in range int current = getSelectedPos(str.toString()); changeCurrent(current); } } /** * Updates the view of this NumberPicker. If displayValues were specified in * {@link #setRange}, 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. */ 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. */ if (mDisplayedValues == null) { mInputText.setText(formatNumber(mCurrent)); } else { mInputText.setText(mDisplayedValues[mCurrent - mStart]); } mInputText.setSelection(mInputText.getText().length()); } /** * Notifies the listener, if registered, of a change of the value of this * NumberPicker. */ private void notifyChange(int previous, int current) { if (mOnChangeListener != null) { mOnChangeListener.onChange(this, previous, mCurrent); } } /** * Posts a command for updating the current value every updateMillis * . */ private void postUpdateValueFromLongPress(int updateMillis) { mInputText.clearFocus(); removeAllCallbacks(); if (mUpdateFromLongPressCommand == null) { mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand(); } mUpdateFromLongPressCommand.setUpdateStep(updateMillis); post(mUpdateFromLongPressCommand); } /** * Removes all pending callback from the message queue. */ private void removeAllCallbacks() { if (mUpdateFromLongPressCommand != null) { removeCallbacks(mUpdateFromLongPressCommand); } if (mAdjustScrollerCommand != null) { removeCallbacks(mAdjustScrollerCommand); } if (mSetSelectionCommand != null) { removeCallbacks(mSetSelectionCommand); } } /** * @return The selected index given its displayed value. */ private int getSelectedPos(String value) { if (mDisplayedValues == null) { try { return Integer.parseInt(value); } catch (NumberFormatException e) { // Ignore as if it's not a number we don't care } } else { for (int i = 0; i < mDisplayedValues.length; i++) { // Don't force the user to type in jan when ja will do value = value.toLowerCase(); if (mDisplayedValues[i].toLowerCase().startsWith(value)) { return mStart + i; } } /* * The user might have typed in a number into the month field i.e. * 10 instead of OCT so support that too. */ try { return Integer.parseInt(value); } catch (NumberFormatException e) { // Ignore as if it's not a number we don't care } } return mStart; } /** * Posts an {@link SetSelectionCommand} from the given selectionStart * to * selectionEnd. */ private void postSetSelectionCommand(int selectionStart, int selectionEnd) { if (mSetSelectionCommand == null) { mSetSelectionCommand = new SetSelectionCommand(); } else { removeCallbacks(mSetSelectionCommand); } mSetSelectionCommand.mSelectionStart = selectionStart; mSetSelectionCommand.mSelectionEnd = selectionEnd; post(mSetSelectionCommand); } /** * Posts an {@link AdjustScrollerCommand} within the given * delayMillis * . */ 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. */ class InputTextFilter extends NumberKeyListener { // XXX This doesn't allow for range limits when controlled by a // soft input method! public int getInputType() { return InputType.TYPE_CLASS_TEXT; } @Override protected char[] getAcceptedChars() { return DIGIT_CHARACTERS; } @Override 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) { filtered = source.subSequence(start, end); } String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + dest.subSequence(dend, dest.length()); if ("".equals(result)) { return result; } int val = getSelectedPos(result); /* * Ensure the user can't type in a value greater than the max * allowed. We have to allow less than min as the user might * want to delete some numbers and then type a new number. */ if (val > mEnd) { return ""; } else { return filtered; } } else { CharSequence filtered = String.valueOf(source.subSequence(start, end)); if (TextUtils.isEmpty(filtered)) { return ""; } String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + dest.subSequence(dend, dest.length()); String str = String.valueOf(result).toLowerCase(); for (String val : mDisplayedValues) { String valLowerCase = val.toLowerCase(); if (valLowerCase.startsWith(str)) { postSetSelectionCommand(result.length(), val.length()); return val.subSequence(dstart, val.length()); } } return ""; } } } /** * Command for setting the input text selection. */ class SetSelectionCommand implements Runnable { private int mSelectionStart; private int mSelectionEnd; public void run() { mInputText.setSelection(mSelectionStart, mSelectionEnd); } } /** * 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; int deltaY = mInitialScrollOffset - mCurrentScrollOffset; float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize; int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS); mAdjustScroller.startScroll(0, 0, 0, deltaY, duration); invalidate(); } } /** * Command for updating the current value from a long press. */ class UpdateValueFromLongPressCommand implements Runnable { private int mUpdateStep = 0; private void setUpdateStep(int updateStep) { mUpdateStep = updateStep; } public void run() { changeCurrent(mCurrent + mUpdateStep); postDelayed(this, mLongPressUpdateInterval); } } }