summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget/NumberPicker.java
diff options
context:
space:
mode:
authorSvetoslav Ganov <svetoslavganov@google.com>2010-11-19 00:04:05 -0800
committerSvetoslav Ganov <svetoslavganov@google.com>2010-11-29 15:47:55 -0800
commit206316a61f904ea0a6b106137dd7715a2c246d4c (patch)
treea44aee5e797435b94cc090ad0fce270550e8f4bc /core/java/android/widget/NumberPicker.java
parent360a102f41098d36dccc4fe245e192c6e2f0ceeb (diff)
downloadframeworks_base-206316a61f904ea0a6b106137dd7715a2c246d4c.zip
frameworks_base-206316a61f904ea0a6b106137dd7715a2c246d4c.tar.gz
frameworks_base-206316a61f904ea0a6b106137dd7715a2c246d4c.tar.bz2
New Number picker widget
Change-Id: I834e725b58682e7a48cc3f3302c93c57b35d4e27
Diffstat (limited to 'core/java/android/widget/NumberPicker.java')
-rw-r--r--core/java/android/widget/NumberPicker.java1380
1 files changed, 1114 insertions, 266 deletions
diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java
index 4482b5b..0c298b0 100644
--- a/core/java/android/widget/NumberPicker.java
+++ b/core/java/android/widget/NumberPicker.java
@@ -18,80 +18,134 @@ 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.os.Handler;
+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.Rect;
+import android.graphics.Paint.Align;
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.MotionEvent;
+import android.view.VelocityTracker;
import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.LayoutInflater.Filter;
+import android.view.animation.OvershootInterpolator;
+import android.view.inputmethod.InputMethodManager;
/**
- * A view for selecting a number
+ * A view for selecting a number For a dialog using this view, see
+ * {@link android.app.TimePickerDialog}.
*
- * For a dialog using this view, see {@link android.app.TimePickerDialog}.
* @hide
*/
@Widget
public class NumberPicker extends LinearLayout {
/**
- * The callback interface used to indicate the number value has been adjusted.
+ * The index of the middle selector item.
*/
- public interface OnChangedListener {
- /**
- * @param picker The NumberPicker associated with this listener.
- * @param oldVal The previous value.
- * @param newVal The new value.
- */
- void onChanged(NumberPicker picker, int oldVal, int newVal);
- }
+ private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2;
/**
- * Interface used to format the number into a string for presentation
+ * The coefficient by which to adjust (divide) the max fling velocity.
*/
- public interface Formatter {
- String toString(int value);
- }
+ private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
- /*
- * 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);
- 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 the duration for adjusting the selector wheel.
+ */
+ private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
- private final Handler mHandler;
- private final Runnable mRunnable = new Runnable() {
- public void run() {
- if (mIncrement) {
- changeCurrent(mCurrent + 1);
- mHandler.postDelayed(this, mSpeed);
- } else if (mDecrement) {
- changeCurrent(mCurrent - 1);
- mHandler.postDelayed(this, mSpeed);
- }
+ /**
+ * 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);
+
+ 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();
}
};
- private final EditText mText;
- private final InputFilter mNumberInputFilter;
+ /**
+ * 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;
/**
@@ -110,104 +164,466 @@ public class NumberPicker extends LinearLayout {
private int mCurrent;
/**
- * Previous value of this NumberPicker.
+ * Listener to be notified upon current value change.
*/
- private int mPrevious;
private OnChangedListener mListener;
+
+ /**
+ * Formatter for for displaying the current value.
+ */
private Formatter mFormatter;
- private long mSpeed = 300;
- private boolean mIncrement;
- private boolean mDecrement;
+ /**
+ * The speed for updating the value form long press.
+ */
+ private long mLongPressUpdateSpeed = 300;
/**
- * Create a new number picker
- * @param context the application environment
+ * Cache for the string representation of selector indices.
+ */
+ private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
+
+ /**
+ * 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.
*/
- public NumberPicker(Context context) {
- this(context, null);
+ private final int mSolidColor;
+
+ /**
+ * Reusable {@link Rect} instance.
+ */
+ private final Rect mTempRect = new Rect();
+
+ /**
+ * The callback interface used to indicate the number value has changed.
+ */
+ public interface OnChangedListener {
+ /**
+ * @param picker The NumberPicker associated with this listener.
+ * @param oldVal The previous value.
+ * @param newVal The new value.
+ */
+ void onChanged(NumberPicker picker, int oldVal, int newVal);
+ }
+
+ /**
+ * Interface used to format the number into a string for presentation
+ */
+ public interface Formatter {
+ String toString(int value);
}
/**
* Create a new number picker
- * @param context the application environment
- * @param attrs a collection of attributes
+ *
+ * @param context The application environment.
+ * @param attrs A collection of attributes.
*/
public NumberPicker(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOrientation(VERTICAL);
- LayoutInflater inflater =
- (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ 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);
- mHandler = new Handler();
- OnClickListener clickListener = new OnClickListener() {
+ OnClickListener onClickListener = new OnClickListener() {
public void onClick(View v) {
- validateInput(mText);
- if (!mText.hasFocus()) mText.requestFocus();
-
- // now perform the increment/decrement
- if (R.id.increment == v.getId()) {
+ mInputText.clearFocus();
+ if (v.getId() == R.id.increment) {
changeCurrent(mCurrent + 1);
- } else if (R.id.decrement == v.getId()) {
+ } else {
changeCurrent(mCurrent - 1);
}
}
};
- OnFocusChangeListener focusListener = new OnFocusChangeListener() {
- public void onFocusChange(View v, boolean hasFocus) {
+ 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;
+ }
+ };
- /* When focus is lost check that the text field
- * has valid values.
- */
+ // 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) {
- validateInput(v);
+ validateInputTextView(v);
}
}
- };
+ });
+ mInputText.setFilters(new InputFilter[] {
+ new InputTextFilter()
+ });
- OnLongClickListener longClickListener = new OnLongClickListener() {
- /**
- * We start the long click here but rely on the {@link NumberPickerButton}
- * to inform us when the long click has ended.
- */
- public boolean onLongClick(View v) {
- /* The text view may still have focus so clear it's focus which will
- * trigger the on focus changed and any typed values to be pulled.
- */
- mText.clearFocus();
-
- if (R.id.increment == v.getId()) {
- mIncrement = true;
- mHandler.post(mRunnable);
- } else if (R.id.decrement == v.getId()) {
- mDecrement = true;
- mHandler.post(mRunnable);
+ 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);
}
- return true;
+ mCanceled = false;
+ mSelectorPaint.setAlpha(255);
+ invalidate();
}
- };
- InputFilter inputFilter = new NumberPickerInputFilter();
- mNumberInputFilter = new NumberRangeKeyListener();
- mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
- mIncrementButton.setOnClickListener(clickListener);
- mIncrementButton.setOnLongClickListener(longClickListener);
- mIncrementButton.setNumberPicker(this);
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mShowInputControlsAnimator.isRunning()) {
+ mCanceled = true;
+ }
+ }
+ });
- mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
- mDecrementButton.setOnClickListener(clickListener);
- mDecrementButton.setOnLongClickListener(longClickListener);
- mDecrementButton.setNumberPicker(this);
+ // create the fling and adjust scrollers
+ mFlingScroller = new Scroller(getContext());
+ mAdjustScroller = new Scroller(getContext(), new OvershootInterpolator());
- mText = (EditText) findViewById(R.id.timepicker_input);
- mText.setOnFocusChangeListener(focusListener);
- mText.setFilters(new InputFilter[] {inputFilter});
- mText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+ 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) {
+ mBeginEditOnUpEvent = mFlingScroller.isFinished()
+ && mAdjustScroller.isFinished();
+ mAdjustScrollerOnUpEvent = true;
+ mFlingScroller.forceFinished(true);
+ mAdjustScroller.forceFinished(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;
+ 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) {
+ int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
+ if (deltaDownY > mTouchSlop) {
+ mBeginEditOnUpEvent = false;
+ }
+ }
+ 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);
+ return true;
+ }
+ VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+ int initialVelocity = (int) velocityTracker.getYVelocity();
+ if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
+ fling(initialVelocity);
+ } 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);
+ }
- if (!isEnabled()) {
- setEnabled(false);
+ @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();
}
}
@@ -222,11 +638,57 @@ public class NumberPicker extends LinearLayout {
super.setEnabled(enabled);
mIncrementButton.setEnabled(enabled);
mDecrementButton.setEnabled(enabled);
- mText.setEnabled(enabled);
+ mInputText.setEnabled(enabled);
+ }
+
+ /**
+ * Scrolls the selector with the given <code>vertical offset</code>.
+ */
+ @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;
+ }
+ }
}
/**
* Set the callback that indicates the number has been adjusted by the user.
+ *
* @param listener the callback, should not be null.
*/
public void setOnChangeListener(OnChangedListener listener) {
@@ -235,292 +697,678 @@ public class NumberPicker extends LinearLayout {
/**
* Set the formatter that will be used to format the number for presentation
- * @param formatter the formatter object. If formatter is null, String.valueOf()
- * will be used
+ *
+ * @param formatter the formatter object. If formatter is null,
+ * String.valueOf() will be used
*/
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.
+ * 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/*displayedValues*/);
+ 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.
+ * 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.
*
* @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.
+ *
+ * @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) {
+ if (start < 0 || end < 0) {
+ throw new IllegalArgumentException("start and end must be > 0");
+ }
+
mDisplayedValues = displayedValues;
mStart = start;
mEnd = end;
mCurrent = start;
- updateView();
+
+ setWrapSelector(wrapSelector);
+ updateInputTextView();
if (displayedValues != null) {
// Allow text entry rather than strictly numeric entry.
- mText.setRawInputType(InputType.TYPE_CLASS_TEXT |
- InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ 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
+ * @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");
+ throw new IllegalArgumentException("current should be >= start and <= end");
}
mCurrent = current;
- updateView();
+ updateInputTextView();
+ updateIncrementAndDecrementButtonsVisibilityState();
+ }
+
+ /**
+ * Sets whether the selector shown during flinging/scrolling should wrap
+ * around the beginning and end values.
+ *
+ * @param wrapSelector Whether to wrap.
+ */
+ public void setWrapSelector(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 will scroll when the +/-
- * buttons are longpressed
+ * Sets the speed at which the numbers will scroll when the +/- buttons are
+ * longpressed
*
* @param speed The speed (in milliseconds) at which the numbers will scroll
- * default 300ms
+ * default 300ms
*/
public void setSpeed(long speed) {
- mSpeed = speed;
+ mLongPressUpdateSpeed = speed;
}
- private String formatNumber(int value) {
- return (mFormatter != null)
- ? mFormatter.toString(value)
- : String.valueOf(value);
+ /**
+ * Returns the current value of the NumberPicker
+ *
+ * @return the current value.
+ */
+ public int getCurrent() {
+ return mCurrent;
+ }
+
+ @Override
+ public int getSolidColor() {
+ return mSolidColor;
+ }
+
+ @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.
+ * Returns the upper value of the range of the NumberPicker
*
- * Subclasses can override this to change the wrapping behavior
+ * @return the uppper number of the range.
+ */
+ protected int getEndRange() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the lower value of the range of the NumberPicker
+ *
+ * @return the lower number of the range.
+ */
+ protected int getBeginRange() {
+ return mStart;
+ }
+
+ /**
+ * 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
*/
- protected void changeCurrent(int current) {
+ private void changeCurrent(int current) {
+ if (mCurrent == current) {
+ return;
+ }
// Wrap around the values if we go past the start or end
- if (current > mEnd) {
- current = mStart;
- } else if (current < mStart) {
- current = mEnd;
+ if (mWrapSelector) {
+ current = getWrappedSelectorIndex(current);
}
- mPrevious = mCurrent;
- mCurrent = current;
- notifyChange();
- updateView();
+ int previous = mCurrent;
+ setCurrent(current);
+ notifyChange(previous, current);
}
/**
- * Notifies the listener, if registered, of a change of the value of this
- * NumberPicker.
+ * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
+ * wheel.
*/
- private void notifyChange() {
- if (mListener != null) {
- mListener.onChanged(this, mPrevious, mCurrent);
+ @SuppressWarnings("unused")
+ // Called by ShowInputControlsAnimator via reflection
+ private void setSelectorPaintAlpha(int alpha) {
+ mSelectorPaint.setAlpha(alpha);
+ if (mDrawSelectorWheel) {
+ invalidate();
}
}
/**
- * 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.
+ * @return If the <code>event</code> is in the input text.
*/
- private void updateView() {
- /* 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) {
- mText.setText(formatNumber(mCurrent));
+ private boolean isEventInInputText(MotionEvent event) {
+ mInputText.getHitRect(mTempRect);
+ return mTempRect.contains((int) event.getX(), (int) event.getY());
+ }
+
+ /**
+ * Sets if to <code>drawSelectionWheel</code>.
+ */
+ 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 <code>scroller</code>.
+ */
+ private void onScrollerFinished(Scroller scroller) {
+ if (scroller == mFlingScroller) {
+ postAdjustScrollerCommand(0);
} else {
- mText.setText(mDisplayedValues[mCurrent - mStart]);
+ showInputControls();
+ updateInputTextView();
}
- mText.setSelection(mText.getText().length());
}
- private void validateCurrentView(CharSequence str) {
- int val = getSelectedPos(str.toString());
- if ((val >= mStart) && (val <= mEnd)) {
- if (mCurrent != val) {
- mPrevious = mCurrent;
- mCurrent = val;
- notifyChange();
+ /**
+ * Flings the selector with the given <code>velocityY</code>.
+ */
+ 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);
}
}
- updateView();
+
+ postAdjustScrollerCommand(flingScroller.getDuration());
+ invalidate();
}
- private void validateInput(View v) {
- String str = String.valueOf(((TextView) v).getText());
- if ("".equals(str)) {
+ /**
+ * 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);
+ }
- // Restore to the old value as we don't allow empty values
- updateView();
- } else {
+ /**
+ * 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();
+ }
- // Check the new value and ensure it's in range
- validateCurrentView(str);
+ /**
+ * 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);
}
}
/**
- * @hide
+ * @return The selector indices array with proper values with the current as
+ * the middle one.
*/
- public void cancelIncrement() {
- mIncrement = false;
+ 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;
}
/**
- * @hide
+ * @return The wrapped index <code>selectorIndex</code> value.
+ * <p>
+ * Note: The absolute value of the argument is never larger than
+ * mEnd - mStart.
+ * </p>
*/
- public void cancelDecrement() {
- mDecrement = false;
+ private int getWrappedSelectorIndex(int selectorIndex) {
+ if (selectorIndex > mEnd) {
+ return (Math.abs(selectorIndex) - mEnd);
+ } else if (selectorIndex < mStart) {
+ return (mEnd - Math.abs(selectorIndex));
+ }
+ return selectorIndex;
}
- private static final char[] DIGIT_CHARACTERS = new char[] {
- '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
- };
+ /**
+ * Increments the <code>selectorIndices</code> 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);
+ }
- private NumberPickerButton mIncrementButton;
- private NumberPickerButton mDecrementButton;
+ /**
+ * Decrements the <code>selectorIndices</code> 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);
+ }
- private class NumberPickerInputFilter implements InputFilter {
- public CharSequence filter(CharSequence source, int start, int end,
- Spanned dest, int dstart, int dend) {
- if (mDisplayedValues == null) {
- return mNumberInputFilter.filter(source, start, end, dest, dstart, dend);
- }
- CharSequence filtered = String.valueOf(source.subSequence(start, end));
- String result = String.valueOf(dest.subSequence(0, dstart))
- + filtered
- + dest.subSequence(dend, dest.length());
- String str = String.valueOf(result).toLowerCase();
- for (String val : mDisplayedValues) {
- val = val.toLowerCase();
- if (val.startsWith(str)) {
- return filtered;
- }
+ /**
+ * Ensures we have a cached string representation of the given <code>
+ * selectorIndex</code>
+ * to avoid multiple instantiations of the same string.
+ */
+ private void ensureCachedScrollSelectorValue(int selectorIndex) {
+ SparseArray<String> cache = mSelectorIndexToStringCache;
+ String scrollSelectorValue = cache.get(selectorIndex);
+ if (scrollSelectorValue != null) {
+ return;
+ }
+ if (selectorIndex < mStart || selectorIndex > mEnd) {
+ scrollSelectorValue = "";
+ } else {
+ if (mDisplayedValues != null) {
+ scrollSelectorValue = mDisplayedValues[selectorIndex];
+ } else {
+ scrollSelectorValue = formatNumber(selectorIndex);
}
- return "";
}
+ cache.put(selectorIndex, scrollSelectorValue);
}
- private class NumberRangeKeyListener extends NumberKeyListener {
+ private String formatNumber(int value) {
+ return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
+ }
- // XXX This doesn't allow for range limits when controlled by a
- // soft input method!
- public int getInputType() {
- return InputType.TYPE_CLASS_NUMBER;
+ 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);
}
+ }
- @Override
- protected char[] getAcceptedChars() {
- return DIGIT_CHARACTERS;
+ /**
+ * 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());
+ }
- @Override
- public CharSequence filter(CharSequence source, int start, int end,
- Spanned dest, int dstart, int dend) {
-
- 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());
+ /**
+ * Notifies the listener, if registered, of a change of the value of this
+ * NumberPicker.
+ */
+ private void notifyChange(int previous, int current) {
+ if (mListener != null) {
+ mListener.onChanged(this, previous, mCurrent);
+ }
+ }
- if ("".equals(result)) {
- return result;
- }
- int val = getSelectedPos(result);
+ /**
+ * Posts a command for updating the current value every <code>updateMillis
+ * </code>.
+ */
+ private void postUpdateValueFromLongPress(int updateMillis) {
+ mInputText.clearFocus();
+ removeAllCallbacks();
+ if (mUpdateFromLongPressCommand == null) {
+ mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand();
+ }
+ mUpdateFromLongPressCommand.setUpdateStep(updateMillis);
+ post(mUpdateFromLongPressCommand);
+ }
- /* 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;
- }
+ /**
+ * 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);
}
}
- private int getSelectedPos(String str) {
+ /**
+ * @return The selected index given its displayed <code>value</code>.
+ */
+ private int getSelectedPos(String value) {
if (mDisplayedValues == null) {
try {
- return Integer.parseInt(str);
+ return Integer.parseInt(value);
} catch (NumberFormatException e) {
- /* Ignore as if it's not a number we don't care */
+ // 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 */
- str = str.toLowerCase();
- if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
+ // 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.
+ /*
+ * 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(str);
+ return Integer.parseInt(value);
} catch (NumberFormatException e) {
- /* Ignore as if it's not a number we don't care */
+ // Ignore as if it's not a number we don't care
}
}
return mStart;
}
/**
- * Returns the current value of the NumberPicker
- * @return the current value.
+ * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
+ * </code> to
+ * <code>selectionEnd</code>.
*/
- public int getCurrent() {
- return mCurrent;
+ private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
+ if (mSetSelectionCommand == null) {
+ mSetSelectionCommand = new SetSelectionCommand();
+ } else {
+ removeCallbacks(mSetSelectionCommand);
+ }
+ mSetSelectionCommand.mSelectionStart = selectionStart;
+ mSetSelectionCommand.mSelectionEnd = selectionEnd;
+ post(mSetSelectionCommand);
}
/**
- * Returns the upper value of the range of the NumberPicker
- * @return the uppper number of the range.
+ * Posts an {@link AdjustScrollerCommand} within the given <code>
+ * delayMillis</code>
+ * .
*/
- protected int getEndRange() {
- return mEnd;
+ private void postAdjustScrollerCommand(int delayMillis) {
+ if (mAdjustScrollerCommand == null) {
+ mAdjustScrollerCommand = new AdjustScrollerCommand();
+ } else {
+ removeCallbacks(mAdjustScrollerCommand);
+ }
+ postDelayed(mAdjustScrollerCommand, delayMillis);
}
/**
- * Returns the lower value of the range of the NumberPicker
- * @return the lower number of the range.
+ * Filter for accepting only valid indices or prefixes of the string
+ * representation of valid indices.
*/
- protected int getBeginRange() {
- return mStart;
+ 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, mLongPressUpdateSpeed);
+ }
}
}