blob: 750cbaa4dd2e388785dc2d78f26c684a82feb7e4 [file] [log] [blame]
/*
* 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.Widget;
import android.content.Context;
import android.content.res.ColorStateList;
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.graphics.drawable.Drawable;
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.util.TypedValue;
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.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.R;
/**
* A widget that enables the user to select a number form a predefined range.
* The widget presents an input field and up and down buttons for selecting the
* current value. Pressing/long-pressing the up and down buttons increments and
* decrements the current value respectively. Touching the input field shows a
* scroll wheel, which when touched allows direct edit
* of the current value. Sliding gestures up or down hide the buttons and the
* input filed, show and rotates the scroll wheel. Flinging is
* also supported. The widget enables mapping from positions to strings such
* that, instead of the position index, the corresponding string is displayed.
* <p>
* For an example of using this widget, see {@link android.widget.TimePicker}.
* </p>
*/
@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 duration of scrolling to the next/previous value while changing
* the current value by one, i.e. increment or decrement.
*/
private static final int CHANGE_CURRENT_BY_ONE_SCROLL_DURATION = 300;
/**
* The the delay for showing the input controls after a single tap on the
* input text.
*/
private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration
.getDoubleTapTimeout();
/**
* The strength of fading in the top and bottom while drawing the selector.
*/
private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
/**
* The default unscaled height of the selection divider.
*/
private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
/**
* In this state the selector wheel is not shown.
*/
private static final int SELECTOR_WHEEL_STATE_NONE = 0;
/**
* In this state the selector wheel is small.
*/
private static final int SELECTOR_WHEEL_STATE_SMALL = 1;
/**
* In this state the selector wheel is large.
*/
private static final int SELECTOR_WHEEL_STATE_LARGE = 2;
/**
* The alpha of the selector wheel when it is bright.
*/
private static final int SELECTOR_WHEEL_BRIGHT_ALPHA = 255;
/**
* The alpha of the selector wheel when it is dimmed.
*/
private static final int SELECTOR_WHEEL_DIM_ALPHA = 60;
/**
* The alpha for the increment/decrement button when it is transparent.
*/
private static final int BUTTON_ALPHA_TRANSPARENT = 0;
/**
* The alpha for the increment/decrement button when it is opaque.
*/
private static final int BUTTON_ALPHA_OPAQUE = 1;
/**
* The property for setting the selector paint.
*/
private static final String PROPERTY_SELECTOR_PAINT_ALPHA = "selectorPaintAlpha";
/**
* The property for setting the increment/decrement button alpha.
*/
private static final String PROPERTY_BUTTON_ALPHA = "alpha";
/**
* The numbers accepted by the input text's {@link Filter}
*/
private static final char[] DIGIT_CHARACTERS = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
/**
* Constant for unspecified size.
*/
private static final int SIZE_UNSPECIFIED = -1;
/**
* 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 min height of this widget.
*/
private final int mMinHeight;
/**
* The max height of this widget.
*/
private final int mMaxHeight;
/**
* The max width of this widget.
*/
private final int mMinWidth;
/**
* The max width of this widget.
*/
private int mMaxWidth;
/**
* Flag whether to compute the max width.
*/
private final boolean mComputeMaxWidth;
/**
* The height of the text.
*/
private final int mTextSize;
/**
* The height of the gap between text elements if the selector wheel.
*/
private int mSelectorTextGapHeight;
/**
* 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 OnValueChangeListener mOnValueChangeListener;
/**
* 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<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 mSelectorWheelPaint;
/**
* 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 changing the current value from long
* press by one.
*/
private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
/**
* {@link Animator} for showing the up/down arrows.
*/
private final AnimatorSet mShowInputControlsAnimator;
/**
* {@link Animator} for dimming the selector wheel.
*/
private final Animator mDimSelectorWheelAnimator;
/**
* 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;
/**
* The state of the selector wheel.
*/
private int mSelectorWheelState;
/**
* 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 mWrapSelectorWheel;
/**
* The back ground color used to optimize scroller fading.
*/
private final int mSolidColor;
/**
* Flag indicating if this widget supports flinging.
*/
private final boolean mFlingable;
/**
* Divider for showing item to be selected while scrolling
*/
private final Drawable mSelectionDivider;
/**
* The height of the selection divider.
*/
private final int mSelectionDividerHeight;
/**
* 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 duration of the animation for showing the input controls.
*/
private final long mShowInputControlsAnimimationDuration;
/**
* Flag whether the scoll wheel and the fading edges have been initialized.
*/
private boolean mScrollWheelAndFadingEdgesInitialized;
/**
* Interface to listen for changes of the current value.
*/
public interface OnValueChangeListener {
/**
* 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 onValueChange(NumberPicker picker, int oldVal, int newVal);
}
/**
* Interface to listen for 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 invoked while the number picker scroll state has changed.
*
* @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 current value into a string for presentation.
*/
public interface Formatter {
/**
* Formats a string representation of the current value.
*
* @param value The currently selected value.
* @return A formatted string representation.
*/
public String format(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);
mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
mFlingable = attributesArray.getBoolean(R.styleable.NumberPicker_flingable, true);
mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider);
int defSelectionDividerHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
getResources().getDisplayMetrics());
mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight);
mMinHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_minHeight,
SIZE_UNSPECIFIED);
mMaxHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_maxHeight,
SIZE_UNSPECIFIED);
if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
&& mMinHeight > mMaxHeight) {
throw new IllegalArgumentException("minHeight > maxHeight");
}
mMinWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_minWidth,
SIZE_UNSPECIFIED);
mMaxWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_maxWidth,
SIZE_UNSPECIFIED);
if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
&& mMinWidth > mMaxWidth) {
throw new IllegalArgumentException("minWidth > maxWidth");
}
mComputeMaxWidth = (mMaxWidth == Integer.MAX_VALUE);
attributesArray.recycle();
mShowInputControlsAnimimationDuration = getResources().getInteger(
R.integer.config_longAnimTime);
// By default Linearlayout that we extend is not drawn. This is
// its draw() method is not called but dispatchDraw() is called
// directly (see ViewGroup.drawChild()). However, this class uses
// the fading edge effect implemented by View and we need our
// draw() method to be called. Therefore, we declare we will draw.
setWillNotDraw(false);
setSelectorWheelState(SELECTOR_WHEEL_STATE_NONE);
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) {
InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
}
mInputText.clearFocus();
if (v.getId() == R.id.increment) {
changeCurrentByOne(true);
} else {
changeCurrentByOne(false);
}
}
};
OnLongClickListener onLongClickListener = new OnLongClickListener() {
public boolean onLongClick(View v) {
mInputText.clearFocus();
if (v.getId() == R.id.increment) {
postChangeCurrentByOneFromLongPress(true);
} else {
postChangeCurrentByOneFromLongPress(false);
}
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.numberpicker_input);
mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
mInputText.selectAll();
InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
if (inputMethodManager != null) {
inputMethodManager.showSoftInput(mInputText, 0);
}
} else {
mInputText.setSelection(0, 0);
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);
mSelectorWheelPaint = paint;
// create the animator for showing the input controls
mDimSelectorWheelAnimator = ObjectAnimator.ofInt(this, PROPERTY_SELECTOR_PAINT_ALPHA,
SELECTOR_WHEEL_BRIGHT_ALPHA, SELECTOR_WHEEL_DIM_ALPHA);
final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton,
PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE);
final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton,
PROPERTY_BUTTON_ALPHA, BUTTON_ALPHA_TRANSPARENT, BUTTON_ALPHA_OPAQUE);
mShowInputControlsAnimator = new AnimatorSet();
mShowInputControlsAnimator.playTogether(mDimSelectorWheelAnimator, showIncrementButton,
showDecrementButton);
mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() {
private boolean mCanceled = false;
@Override
public void onAnimationEnd(Animator animation) {
if (!mCanceled) {
// if canceled => we still want the wheel drawn
setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL);
}
mCanceled = false;
}
@Override
public void onAnimationCancel(Animator animation) {
if (mShowInputControlsAnimator.isRunning()) {
mCanceled = true;
}
}
});
// create the fling and adjust scrollers
mFlingScroller = new Scroller(getContext(), null, true);
mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
updateInputTextView();
updateIncrementAndDecrementButtonsVisibilityState();
if (mFlingable) {
if (isInEditMode()) {
setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL);
} else {
// Start with shown selector wheel and hidden controls. When made
// visible hide the selector and fade-in the controls to suggest
// fling interaction.
setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
hideInputControls();
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int msrdWdth = getMeasuredWidth();
final int msrdHght = getMeasuredHeight();
// Increment button at the top.
final int inctBtnMsrdWdth = mIncrementButton.getMeasuredWidth();
final int incrBtnLeft = (msrdWdth - inctBtnMsrdWdth) / 2;
final int incrBtnTop = 0;
final int incrBtnRight = incrBtnLeft + inctBtnMsrdWdth;
final int incrBtnBottom = incrBtnTop + mIncrementButton.getMeasuredHeight();
mIncrementButton.layout(incrBtnLeft, incrBtnTop, incrBtnRight, incrBtnBottom);
// Input text centered horizontally.
final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
// Decrement button at the top.
final int decrBtnMsrdWdth = mIncrementButton.getMeasuredWidth();
final int decrBtnLeft = (msrdWdth - decrBtnMsrdWdth) / 2;
final int decrBtnTop = msrdHght - mDecrementButton.getMeasuredHeight();
final int decrBtnRight = decrBtnLeft + decrBtnMsrdWdth;
final int decrBtnBottom = msrdHght;
mDecrementButton.layout(decrBtnLeft, decrBtnTop, decrBtnRight, decrBtnBottom);
if (!mScrollWheelAndFadingEdgesInitialized) {
mScrollWheelAndFadingEdgesInitialized = true;
// need to do all this when we know our size
initializeSelectorWheel();
initializeFadingEdges();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Try greedily to fit the max width and height.
final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
// Flag if we are measured with width or height less than the respective min.
final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
widthMeasureSpec);
final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!isEnabled() || !mFlingable) {
return false;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastMotionEventY = mLastDownEventY = event.getY();
removeAllCallbacks();
mShowInputControlsAnimator.cancel();
mDimSelectorWheelAnimator.cancel();
mBeginEditOnUpEvent = false;
mAdjustScrollerOnUpEvent = true;
if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA);
boolean scrollersFinished = mFlingScroller.isFinished()
&& mAdjustScroller.isFinished();
if (!scrollersFinished) {
mFlingScroller.forceFinished(true);
mAdjustScroller.forceFinished(true);
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
mBeginEditOnUpEvent = scrollersFinished;
mAdjustScrollerOnUpEvent = true;
hideInputControls();
return true;
}
if (isEventInVisibleViewHitRect(event, mIncrementButton)
|| isEventInVisibleViewHitRect(event, mDecrementButton)) {
return false;
}
mAdjustScrollerOnUpEvent = false;
setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
hideInputControls();
return true;
case MotionEvent.ACTION_MOVE:
float currentMoveY = event.getY();
int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
if (deltaDownY > mTouchSlop) {
mBeginEditOnUpEvent = false;
onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
hideInputControls();
return true;
}
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
return false;
}
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;
onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
int deltaMoveY = (int) (currentMoveY - mLastMotionEventY);
scrollBy(0, deltaMoveY);
invalidate();
mLastMotionEventY = currentMoveY;
break;
case MotionEvent.ACTION_UP:
if (mBeginEditOnUpEvent) {
setSelectorWheelState(SELECTOR_WHEEL_STATE_SMALL);
showInputControls(mShowInputControlsAnimimationDuration);
mInputText.requestFocus();
return true;
}
VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity();
if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
fling(initialVelocity);
onScrollStateChange(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) {
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_MOVE:
if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
removeAllCallbacks();
forceCompleteChangeCurrentByOneViaScroll();
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
removeAllCallbacks();
break;
}
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 (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) {
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();
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mIncrementButton.setEnabled(enabled);
mDecrementButton.setEnabled(enabled);
mInputText.setEnabled(enabled);
}
@Override
public void scrollBy(int x, int y) {
if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) {
return;
}
int[] selectorIndices = mSelectorIndices;
if (!mWrapSelectorWheel && y > 0
&& selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
mCurrentScrollOffset = mInitialScrollOffset;
return;
}
if (!mWrapSelectorWheel && y < 0
&& selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
mCurrentScrollOffset = mInitialScrollOffset;
return;
}
mCurrentScrollOffset += y;
while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
mCurrentScrollOffset -= mSelectorElementHeight;
decrementSelectorIndices(selectorIndices);
changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
mCurrentScrollOffset = mInitialScrollOffset;
}
}
while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
mCurrentScrollOffset += mSelectorElementHeight;
incrementSelectorIndices(selectorIndices);
changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]);
if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
mCurrentScrollOffset = mInitialScrollOffset;
}
}
}
@Override
public int getSolidColor() {
return mSolidColor;
}
/**
* Sets the listener to be notified on change of the current value.
*
* @param onValueChangedListener The listener.
*/
public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
mOnValueChangeListener = onValueChangedListener;
}
/**
* Set listener to be notified for scroll state changes.
*
* @param onScrollListener The listener.
*/
public void setOnScrollListener(OnScrollListener onScrollListener) {
mOnScrollListener = onScrollListener;
}
/**
* Set the formatter to be used for formatting the current value.
* <p>
* Note: If you have provided alternative values for the values this
* formatter is never invoked.
* </p>
*
* @param formatter The formatter object. If formatter is <code>null</code>,
* {@link String#valueOf(int)} will be used.
*
* @see #setDisplayedValues(String[])
*/
public void setFormatter(Formatter formatter) {
if (formatter == mFormatter) {
return;
}
mFormatter = formatter;
initializeSelectorWheelIndices();
updateInputTextView();
}
/**
* Set the current value for the number picker.
* <p>
* If the argument is less than the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
* current value is set to the {@link NumberPicker#getMinValue()} value.
* </p>
* <p>
* If the argument is less than the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
* current value is set to the {@link NumberPicker#getMaxValue()} value.
* </p>
* <p>
* If the argument is less than the {@link NumberPicker#getMaxValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
* current value is set to the {@link NumberPicker#getMaxValue()} value.
* </p>
* <p>
* If the argument is less than the {@link NumberPicker#getMaxValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
* current value is set to the {@link NumberPicker#getMinValue()} value.
* </p>
*
* @param value The current value.
* @see #setWrapSelectorWheel(boolean)
* @see #setMinValue(int)
* @see #setMaxValue(int)
*/
public void setValue(int value) {
if (mValue == value) {
return;
}
if (value < mMinValue) {
value = mWrapSelectorWheel ? mMaxValue : mMinValue;
}
if (value > mMaxValue) {
value = mWrapSelectorWheel ? mMinValue : mMaxValue;
}
mValue = value;
initializeSelectorWheelIndices();
updateInputTextView();
updateIncrementAndDecrementButtonsVisibilityState();
invalidate();
}
/**
* Computes the max width if no such specified as an attribute.
*/
private void tryComputeMaxWidth() {
if (!mComputeMaxWidth) {
return;
}
int maxTextWidth = 0;
if (mDisplayedValues == null) {
float maxDigitWidth = 0;
for (int i = 0; i <= 9; i++) {
final float digitWidth = mSelectorWheelPaint.measureText(String.valueOf(i));
if (digitWidth > maxDigitWidth) {
maxDigitWidth = digitWidth;
}
}
int numberOfDigits = 0;
int current = mMaxValue;
while (current > 0) {
numberOfDigits++;
current = current / 10;
}
maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
} else {
final int valueCount = mDisplayedValues.length;
for (int i = 0; i < valueCount; i++) {
final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
if (textWidth > maxTextWidth) {
maxTextWidth = (int) textWidth;
}
}
}
maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
if (mMaxWidth != maxTextWidth) {
if (maxTextWidth > mMinWidth) {
mMaxWidth = maxTextWidth;
} else {
mMaxWidth = mMinWidth;
}
invalidate();
}
}
/**
* Gets whether the selector wheel wraps when reaching the min/max value.
*
* @return True if the selector wheel wraps.
*
* @see #getMinValue()
* @see #getMaxValue()
*/
public boolean getWrapSelectorWheel() {
return mWrapSelectorWheel;
}
/**
* Sets whether the selector wheel shown during flinging/scrolling should
* wrap around the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getMaxValue()} values.
* <p>
* By default if the range (max - min) is more than five (the number of
* items shown on the selector wheel) the selector wheel wrapping is
* enabled.
* </p>
*
* @param wrapSelectorWheel Whether to wrap.
*/
public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
if (wrapSelectorWheel && (mMaxValue - mMinValue) < mSelectorIndices.length) {
throw new IllegalStateException("Range less than selector items count.");
}
if (wrapSelectorWheel != mWrapSelectorWheel) {
mWrapSelectorWheel = wrapSelectorWheel;
updateIncrementAndDecrementButtonsVisibilityState();
}
}
/**
* Sets the speed at which the numbers be incremented and decremented when
* the up and down buttons are long pressed respectively.
* <p>
* The default value is 300 ms.
* </p>
*
* @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);
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
}
/**
* 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);
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
}
/**
* 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();
initializeSelectorWheelIndices();
tryComputeMaxWidth();
}
@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 onAttachedToWindow() {
super.onAttachedToWindow();
// make sure we show the controls only the very
// first time the user sees this widget
if (mFlingable && !isInEditMode()) {
// animate a bit slower the very first time
showInputControls(mShowInputControlsAnimimationDuration * 2);
}
}
@Override
protected void onDetachedFromWindow() {
removeAllCallbacks();
}
@Override
protected void dispatchDraw(Canvas canvas) {
// There is a good reason for doing this. See comments in draw().
}
@Override
public void draw(Canvas canvas) {
// Dispatch draw to our children only if we are not currently running
// the animation for simultaneously dimming the scroll wheel and
// showing in the buttons. This class takes advantage of the View
// implementation of fading edges effect to draw the selector wheel.
// However, in View.draw(), the fading is applied after all the children
// have been drawn and we do not want this fading to be applied to the
// buttons. Therefore, we draw our children after we have completed
// drawing ourselves.
super.draw(canvas);
// Draw our children if we are not showing the selector wheel of fading
// it out
if (mShowInputControlsAnimator.isRunning()
|| mSelectorWheelState != SELECTOR_WHEEL_STATE_LARGE) {
long drawTime = getDrawingTime();
for (int i = 0, count = getChildCount(); i < count; i++) {
View child = getChildAt(i);
if (!child.isShown()) {
continue;
}
drawChild(canvas, getChildAt(i), drawTime);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mSelectorWheelState == SELECTOR_WHEEL_STATE_NONE) {
return;
}
float x = (mRight - mLeft) / 2;
float y = mCurrentScrollOffset;
final int restoreCount = canvas.save();
if (mSelectorWheelState == SELECTOR_WHEEL_STATE_SMALL) {
Rect clipBounds = canvas.getClipBounds();
clipBounds.inset(0, mSelectorElementHeight);
canvas.clipRect(clipBounds);
}
// draw the selector wheel
int[] selectorIndices = mSelectorIndices;
for (int i = 0; i < selectorIndices.length; i++) {
int selectorIndex = selectorIndices[i];
String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
// Do not draw the middle item if input is visible since the input is shown only
// if the wheel is static and it covers the middle item. Otherwise, if the user
// starts editing the text via the IME he may see a dimmed version of the old
// value intermixed with the new one.
if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) {
canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
}
y += mSelectorElementHeight;
}
// draw the selection dividers (only if scrolling and drawable specified)
if (mSelectionDivider != null) {
// draw the top divider
int topOfTopDivider =
(getHeight() - mSelectorElementHeight - mSelectionDividerHeight) / 2;
int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
mSelectionDivider.draw(canvas);
// draw the bottom divider
int topOfBottomDivider = topOfTopDivider + mSelectorElementHeight;
int bottomOfBottomDivider = bottomOfTopDivider + mSelectorElementHeight;
mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
mSelectionDivider.draw(canvas);
}
canvas.restoreToCount(restoreCount);
}
@Override
public void sendAccessibilityEvent(int eventType) {
// Do not send accessibility events - we want the user to
// perceive this widget as several controls rather as a whole.
}
/**
* Makes a measure spec that tries greedily to use the max value.
*
* @param measureSpec The measure spec.
* @param maxSize The max value for the size.
* @return A measure spec greedily imposing the max size.
*/
private int makeMeasureSpec(int measureSpec, int maxSize) {
if (maxSize == SIZE_UNSPECIFIED) {
return measureSpec;
}
final int size = MeasureSpec.getSize(measureSpec);
final int mode = MeasureSpec.getMode(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return measureSpec;
case MeasureSpec.AT_MOST:
return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
case MeasureSpec.UNSPECIFIED:
return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
default:
throw new IllegalArgumentException("Unknown measure mode: " + mode);
}
}
/**
* Utility to reconcile a desired size and state, with constraints imposed by
* a MeasureSpec. Tries to respect the min size, unless a different size is
* imposed by the constraints.
*
* @param minSize The minimal desired size.
* @param measuredSize The currently measured size.
* @param measureSpec The current measure spec.
* @return The resolved size and state.
*/
private int resolveSizeAndStateRespectingMinSize(int minSize, int measuredSize,
int measureSpec) {
if (minSize != SIZE_UNSPECIFIED) {
final int desiredWidth = Math.max(minSize, measuredSize);
return resolveSizeAndState(desiredWidth, measureSpec, 0);
} else {
return measuredSize;
}
}
/**
* Resets the selector indices and clear the cached
* string representation of these indices.
*/
private void initializeSelectorWheelIndices() {
mSelectorIndexToStringCache.clear();
int[] selectorIdices = mSelectorIndices;
int current = getValue();
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]);
}
}
/**
* 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);
}
/**
* Changes the current value by one which is increment or
* decrement based on the passes argument.
*
* @param increment True to increment, false to decrement.
*/
private void changeCurrentByOne(boolean increment) {
if (mFlingable) {
mDimSelectorWheelAnimator.cancel();
mInputText.setVisibility(View.INVISIBLE);
mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA);
mPreviousScrollerY = 0;
forceCompleteChangeCurrentByOneViaScroll();
if (increment) {
mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight,
CHANGE_CURRENT_BY_ONE_SCROLL_DURATION);
} else {
mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight,
CHANGE_CURRENT_BY_ONE_SCROLL_DURATION);
}
invalidate();
} else {
if (increment) {
changeCurrent(mValue + 1);
} else {
changeCurrent(mValue - 1);
}
}
}
/**
* Ensures that if we are in the process of changing the current value
* by one via scrolling the scroller gets to its final state and the
* value is updated.
*/
private void forceCompleteChangeCurrentByOneViaScroll() {
Scroller scroller = mFlingScroller;
if (!scroller.isFinished()) {
final int yBeforeAbort = scroller.getCurrY();
scroller.abortAnimation();
final int yDelta = scroller.getCurrY() - yBeforeAbort;
scrollBy(0, yDelta);
}
}
/**
* Sets the <code>alpha</code> of the {@link Paint} for drawing the selector
* wheel.
*/
@SuppressWarnings("unused")
// Called via reflection
private void setSelectorPaintAlpha(int alpha) {
mSelectorWheelPaint.setAlpha(alpha);
invalidate();
}
/**
* @return If the <code>event</code> is in the visible <code>view</code>.
*/
private boolean isEventInVisibleViewHitRect(MotionEvent event, View view) {
if (view.getVisibility() == VISIBLE) {
view.getHitRect(mTempRect);
return mTempRect.contains((int) event.getX(), (int) event.getY());
}
return false;
}
/**
* Sets the <code>selectorWheelState</code>.
*/
private void setSelectorWheelState(int selectorWheelState) {
mSelectorWheelState = selectorWheelState;
if (selectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
mSelectorWheelPaint.setAlpha(SELECTOR_WHEEL_BRIGHT_ALPHA);
}
if (mFlingable && selectorWheelState == SELECTOR_WHEEL_STATE_LARGE
&& AccessibilityManager.getInstance(mContext).isEnabled()) {
AccessibilityManager.getInstance(mContext).interrupt();
String text = mContext.getString(R.string.number_picker_increment_scroll_action);
mInputText.setContentDescription(text);
mInputText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
mInputText.setContentDescription(null);
}
}
private void initializeSelectorWheel() {
initializeSelectorWheelIndices();
int[] selectorIndices = mSelectorIndices;
int totalTextHeight = selectorIndices.length * mTextSize;
float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
float textGapCount = selectorIndices.length - 1;
mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
// Ensure that the middle item is positioned the same as the text in mInputText
int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
mInitialScrollOffset = editTextTextPosition -
(mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
mCurrentScrollOffset = mInitialScrollOffset;
updateInputTextView();
}
private void initializeFadingEdges() {
setVerticalFadingEdgeEnabled(true);
setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
}
/**
* Callback invoked upon completion of a given <code>scroller</code>.
*/
private void onScrollerFinished(Scroller scroller) {
if (scroller == mFlingScroller) {
if (mSelectorWheelState == SELECTOR_WHEEL_STATE_LARGE) {
postAdjustScrollerCommand(0);
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
updateInputTextView();
fadeSelectorWheel(mShowInputControlsAnimimationDuration);
}
} else {
updateInputTextView();
showInputControls(mShowInputControlsAnimimationDuration);
}
}
/**
* Handles transition to a given <code>scrollState</code>
*/
private void onScrollStateChange(int scrollState) {
if (mScrollState == scrollState) {
return;
}
mScrollState = scrollState;
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChange(this, scrollState);
}
}
/**
* Flings the selector with the given <code>velocityY</code>.
*/
private void fling(int velocityY) {
mPreviousScrollerY = 0;
if (velocityY > 0) {
mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
} else {
mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
}
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.
*
* @param animationDuration The duration of the animation.
*/
private void showInputControls(long animationDuration) {
updateIncrementAndDecrementButtonsVisibilityState();
mInputText.setVisibility(VISIBLE);
mShowInputControlsAnimator.setDuration(animationDuration);
mShowInputControlsAnimator.start();
}
/**
* Fade the selector wheel via an animation.
*
* @param animationDuration The duration of the animation.
*/
private void fadeSelectorWheel(long animationDuration) {
mInputText.setVisibility(VISIBLE);
mDimSelectorWheelAnimator.setDuration(animationDuration);
mDimSelectorWheelAnimator.start();
}
/**
* Updates the visibility state of the increment and decrement buttons.
*/
private void updateIncrementAndDecrementButtonsVisibilityState() {
if (mWrapSelectorWheel || mValue < mMaxValue) {
mIncrementButton.setVisibility(VISIBLE);
} else {
mIncrementButton.setVisibility(INVISIBLE);
}
if (mWrapSelectorWheel || mValue > mMinValue) {
mDecrementButton.setVisibility(VISIBLE);
} else {
mDecrementButton.setVisibility(INVISIBLE);
}
}
/**
* @return The wrapped index <code>selectorIndex</code> value.
*/
private int getWrappedSelectorIndex(int selectorIndex) {
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 <code>selectorIndices</code> whose string representations
* will be displayed in the selector.
*/
private void incrementSelectorIndices(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 <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 (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
nextScrollSelectorIndex = mMaxValue;
}
selectorIndices[0] = nextScrollSelectorIndex;
ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
}
/**
* 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 < mMinValue || selectorIndex > mMaxValue) {
scrollSelectorValue = "";
} else {
if (mDisplayedValues != null) {
int displayedValueIndex = selectorIndex - mMinValue;
scrollSelectorValue = mDisplayedValues[displayedValueIndex];
} else {
scrollSelectorValue = formatNumber(selectorIndex);
}
}
cache.put(selectorIndex, scrollSelectorValue);
}
private String formatNumber(int value) {
return (mFormatter != null) ? mFormatter.format(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
* 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(mValue));
} else {
mInputText.setText(mDisplayedValues[mValue - mMinValue]);
}
mInputText.setSelection(mInputText.getText().length());
if (mFlingable && AccessibilityManager.getInstance(mContext).isEnabled()) {
String text = mContext.getString(R.string.number_picker_increment_scroll_mode,
mInputText.getText());
mInputText.setContentDescription(text);
}
}
/**
* Notifies the listener, if registered, of a change of the value of this
* NumberPicker.
*/
private void notifyChange(int previous, int current) {
if (mOnValueChangeListener != null) {
mOnValueChangeListener.onValueChange(this, previous, mValue);
}
}
/**
* Posts a command for changing the current value by one.
*
* @param increment Whether to increment or decrement the value.
*/
private void postChangeCurrentByOneFromLongPress(boolean increment) {
mInputText.clearFocus();
removeAllCallbacks();
if (mChangeCurrentByOneFromLongPressCommand == null) {
mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
}
mChangeCurrentByOneFromLongPressCommand.setIncrement(increment);
post(mChangeCurrentByOneFromLongPressCommand);
}
/**
* Removes all pending callback from the message queue.
*/
private void removeAllCallbacks() {
if (mChangeCurrentByOneFromLongPressCommand != null) {
removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
}
if (mAdjustScrollerCommand != null) {
removeCallbacks(mAdjustScrollerCommand);
}
if (mSetSelectionCommand != null) {
removeCallbacks(mSetSelectionCommand);
}
}
/**
* @return The selected index given its displayed <code>value</code>.
*/
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 <code>selectionStart
* </code> to
* <code>selectionEnd</code>.
*/
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 <code>
* delayMillis</code>
* .
*/
private void postAdjustScrollerCommand(int delayMillis) {
if (mAdjustScrollerCommand == null) {
mAdjustScrollerCommand = new AdjustScrollerCommand();
} else {
removeCallbacks(mAdjustScrollerCommand);
}
postDelayed(mAdjustScrollerCommand, delayMillis);
}
/**
* Filter for accepting only valid indices or prefixes of the string
* representation of valid indices.
*/
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;
if (mInitialScrollOffset == mCurrentScrollOffset) {
updateInputTextView();
showInputControls(mShowInputControlsAnimimationDuration);
return;
}
// adjust to the closest value
int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
}
mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
invalidate();
}
}
/**
* Command for changing the current value from a long press by one.
*/
class ChangeCurrentByOneFromLongPressCommand implements Runnable {
private boolean mIncrement;
private void setIncrement(boolean increment) {
mIncrement = increment;
}
public void run() {
changeCurrentByOne(mIncrement);
postDelayed(this, mLongPressUpdateInterval);
}
}
}