/* * 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.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 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}. *
* * @attr ref android.R.styleable#NumberPicker_solidColor */ @Widget public class NumberPicker extends LinearLayout { /** * The default update interval during long press. */ private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; /** * 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(). * * @hide */ 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 format(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 mMinValue; /** * Upper value of the range of numbers allowed for the NumberPicker */ private int mMaxValue; /** * Current value of this NumberPicker */ private int mValue; /** * Listener to be notified upon current value change. */ private OnValueChangedListener mOnValueChangedListener; /** * 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 = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; /** * Cache for the string representation of selector indices. */ private final SparseArray* Note: If you have provided alternative values for the values this * formatter is never invoked. *
* * @param formatter The formatter object. If formatter isnull
,
* {@link String#valueOf(int)} will be used.
*
* @see #setDisplayedValues(String[])
*/
public void setFormatter(Formatter formatter) {
if (formatter == mFormatter) {
return;
}
mFormatter = formatter;
resetSelectorWheelIndices();
}
/**
* Set the current value for the number picker.
*
* If the argument is less than the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is false
the
* current value is set to the {@link NumberPicker#getMinValue()} value.
*
* If the argument is less than the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is true
the
* current value is set to the {@link NumberPicker#getMaxValue()} value.
*
* If the argument is less than the {@link NumberPicker#getMaxValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is false
the
* current value is set to the {@link NumberPicker#getMaxValue()} value.
*
* If the argument is less than the {@link NumberPicker#getMaxValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is true
the
* current value is set to the {@link NumberPicker#getMinValue()} value.
*
* 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. *
* * @param wrapSelector Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelector) { if (wrapSelector && (mMaxValue - mMinValue) < mSelectorIndices.length) { throw new IllegalStateException("Range less than selector items count."); } if (wrapSelector != mWrapSelectorWheel) { // force the selector indices array to be reinitialized mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE; mWrapSelectorWheel = wrapSelector; } } /** * Sets the speed at which the numbers be incremented and decremented when * the up and down buttons are long pressed respectively. ** The default value is 300 ms. *
* * @param intervalMillis The speed (in milliseconds) at which the numbers * will be incremented and decremented. */ public void setOnLongPressUpdateInterval(long intervalMillis) { mLongPressUpdateInterval = intervalMillis; } /** * Returns the value of the picker. * * @return The value. */ public int getValue() { return mValue; } /** * Returns the min value of the picker. * * @return The min value */ public int getMinValue() { return mMinValue; } /** * Sets the min value of the picker. * * @param minValue The min value. */ public void setMinValue(int minValue) { if (mMinValue == minValue) { return; } if (minValue < 0) { throw new IllegalArgumentException("minValue must be >= 0"); } mMinValue = minValue; if (mMinValue > mValue) { mValue = mMinValue; } boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; setWrapSelectorWheel(wrapSelectorWheel); resetSelectorWheelIndices(); updateInputTextView(); updateIncrementAndDecrementButtonsVisibilityState(); } /** * Returns the max value of the picker. * * @return The max value. */ public int getMaxValue() { return mMaxValue; } /** * Sets the max value of the picker. * * @param maxValue The max value. */ public void setMaxValue(int maxValue) { if (mMaxValue == maxValue) { return; } if (maxValue < 0) { throw new IllegalArgumentException("maxValue must be >= 0"); } mMaxValue = maxValue; if (mMaxValue < mValue) { mValue = mMaxValue; } boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; setWrapSelectorWheel(wrapSelectorWheel); resetSelectorWheelIndices(); updateInputTextView(); updateIncrementAndDecrementButtonsVisibilityState(); } /** * Gets the values to be displayed instead of string values. * * @return The displayed values. */ public String[] getDisplayedValues() { return mDisplayedValues; } /** * Sets the values to be displayed. * * @param displayedValues The displayed values. */ public void setDisplayedValues(String[] displayedValues) { if (mDisplayedValues == displayedValues) { return; } mDisplayedValues = displayedValues; if (mDisplayedValues != 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); } updateInputTextView(); resetSelectorWheelIndices(); } @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. 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; } } /** * Resets the selector indices and clear the cached * string representation of these indices. */ private void resetSelectorWheelIndices() { mSelectorIndexToStringCache.clear(); int[] selectorIdices = getSelectorIndices(); for (int i = 0; i < selectorIdices.length; i++) { selectorIdices[i] = Integer.MIN_VALUE; } } /** * 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 (mValue == current) { return; } // Wrap around the values if we go past the start or end if (mWrapSelectorWheel) { current = getWrappedSelectorIndex(current); } int previous = mValue; setValue(current); notifyChange(previous, current); } /** * Sets thealpha
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 (mWrapSelectorWheel) {
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 * (mValue - mMinValue);
flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY);
} else {
int startY = mTextSize * (mMaxValue - mValue);
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 (mWrapSelectorWheel || mValue < mMaxValue) {
mIncrementButton.setVisibility(VISIBLE);
} else {
mIncrementButton.setVisibility(INVISIBLE);
}
if (mWrapSelectorWheel || mValue > mMinValue) {
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 = getValue();
if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) {
for (int i = 0; i < mSelectorIndices.length; i++) {
int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
if (mWrapSelectorWheel) {
selectorIndex = getWrappedSelectorIndex(selectorIndex);
}
mSelectorIndices[i] = selectorIndex;
ensureCachedScrollSelectorValue(mSelectorIndices[i]);
}
}
return mSelectorIndices;
}
/**
* @return The wrapped index selectorIndex
value.
*/
private int getWrappedSelectorIndex(int selectorIndex) {
if (selectorIndex > mMaxValue) {
return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
} else if (selectorIndex < mMinValue) {
return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
}
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 (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
nextScrollSelectorIndex = mMinValue;
}
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 (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
nextScrollSelectorIndex = mMaxValue;
}
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) {
SparseArrayupdateMillis
*
.
*/
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 mMinValue + 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 mMinValue;
}
/**
* 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 > mMaxValue) {
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(mValue + mUpdateStep);
postDelayed(this, mLongPressUpdateInterval);
}
}
}