Android 自定义NumberPicker
董小林
2023-12-01
public class NumberPicker extends LinearLayout{
private static final float GOLD_ROTE = 1.618f;
/**
* The number of items show in the selector wheel.
*/
private static final int DEFAULT_DISPLAY_ITEM_COUNT = 5;
private static final int DEFAULT_ITEM_HEIGHT = 64;
private static final int DEFAULT_ITEM_WIDTH = (int)(DEFAULT_ITEM_HEIGHT * 1.618);
private static final int DEFAULT_SHADER_LENGHT = 5;
private static final int DEFAULT_MAX_VALUE = 2100;
private static final int DEFAULT_MIN_VALUE = 1;
private static final int DEFAULT_VALUE = DEFAULT_MIN_VALUE + (DEFAULT_MAX_VALUE - DEFAULT_MIN_VALUE) / 2;
/**
* this value will be limit the max velocity in fling
*/
private static final int DEFAULT_MAX_VELOCITY_MASK = 2;
private static final int DEFAULT_CHILD_SIZE = 24;
private static final int DEFAULT_CHILD_COLOR = Color.WHITE;
/**
* the number of item is displayed on the screen
*/
private int mDisplayItemCount;
private int mChildCount;
private int mMiddleChild;
/**
* the size of the both side edges of the gradation pattern
*/
private int mShaderHeight;
/**
* the color of the both side edges of the gradation pattern
*/
private int mShaderColor;
private int mSelectorLineColor;
private int mSelectorlineHeight;
/**
* the color of item word
*/
private int mItemColor;
private int mItemHeight;
private int mItemWidth;
private int mValue;
private int mLastValue;
/**
* use to record the final value in every fling time
*/
private int mLastY;
private int mLastScrollY;
private int mMaxValue;
private int mMinValue;
/**
* Distance in pixels a touch can wander before we think the user is scrolling
*/
private int mTouchSlop;
/**
* The current scroll state of the number picker.
*/
private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
/**
* this view Offset when setup
*/
private int mInitialOffset;
/**
* @see ViewConfiguration#getScaledMinimumFlingVelocity()
*/
private int mMinimumFlingVelocity;
/**
* Control whether to cycle to scroll the wheel
*/
private boolean mWrapSelectorWheel;
/**
* Control whether to draw shader
*/
private boolean mShaderEnable;
/**
* @see ViewConfiguration#getScaledMaximumFlingVelocity()
*/
private int mMaximumFlingVelocity;
private String[] mDisplayedValues;
private Context mContext;
/**
* draw a fading edge on screen top and bottom
*/
private Shader mShader;
private Paint mShaderPaint;
private Paint mChildPaint;
/**
* draw a picture to tell us where is the center
*/
private Drawable mSelector;
private Paint mSelectorPaint;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
/**
* Listener to be notified upon scroll state change.
*/
private OnScrollListener mOnScrollListener;
/**
* Listener to be notified upon current value change.
*/
private OnValueChangeListener mOnValueChangeListener;
/**
* Formatter for for displaying the current value.
*/
private Formatter mFormatter;
/**
* 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 his 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 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 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);
}
public NumberPicker(Context context) {
this(context, null);
// TODO Auto-generated constructor stub
}
public NumberPicker(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberPicker);
if(a != null){
final int defaultH = getResources().getDimensionPixelSize(R.dimen.default_uidatepicker_picker_item_height);
mSelector = a.getDrawable(R.styleable.NumberPicker_number_picker_selector);
mItemWidth = a.getDimensionPixelSize(R.styleable.NumberPicker_number_picker_item_width, (int)(GOLD_ROTE * defaultH));
mItemHeight = a.getDimensionPixelSize(R.styleable.NumberPicker_number_picker_item_height, defaultH);
mShaderHeight = a.getDimensionPixelSize(R.styleable.NumberPicker_number_picker_shader_lenght, DEFAULT_SHADER_LENGHT);
mMinValue = a.getInteger(R.styleable.NumberPicker_number_picker_min_value, DEFAULT_MIN_VALUE);
mMaxValue = a.getInteger(R.styleable.NumberPicker_number_picker_max_value, DEFAULT_MAX_VALUE);
mDisplayItemCount = a.getInteger(R.styleable.NumberPicker_number_picker_display_item_count, DEFAULT_DISPLAY_ITEM_COUNT);
mValue = a.getInteger(R.styleable.NumberPicker_number_picker_vaule, DEFAULT_VALUE);
if(mMinValue > mMaxValue){
throw new IllegalArgumentException("minValue > maxValue");
}
if(mValue < mMinValue || mValue > mMaxValue){
mValue = (mMaxValue - mMinValue) / 2 + mMinValue;
}
mLastValue = mValue;
mWrapSelectorWheel = a.getBoolean(R.styleable.NumberPicker_number_picker_wrap_wheel, true);
mChildPaint = new Paint();
mChildPaint.setTextSize(a.getDimensionPixelSize(R.styleable.NumberPicker_number_picker_child_size, DEFAULT_CHILD_SIZE));
mChildPaint.setColor(a.getColor(R.styleable.NumberPicker_number_picker_child_color, mChildPaint.getColor()));
mShaderColor = a.getColor(R.styleable.NumberPicker_number_picker_shader_color, 0X55000000);
mShaderHeight = a.getInteger(R.styleable.NumberPicker_number_picker_shader_height, defaultH / 3);
mSelectorlineHeight = a.getInteger(R.styleable.NumberPicker_number_picker_selector_line_height, 3);
mSelectorPaint = new Paint();
mSelectorPaint.setAntiAlias(true);
mSelectorPaint.setColor(a.getColor(R.styleable.NumberPicker_number_picker_selector_color, 0XFF0EA5F3));
a.recycle();
}
init();
}
private void init(){
mShaderPaint = new Paint();
mShaderPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
mChildPaint.setTextAlign(Align.CENTER);
mChildPaint.setAntiAlias(true);
setClickable(true);
setOrientation(VERTICAL);
setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
setWillNotDraw(false);
mScroller = new Scroller(getContext());
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / DEFAULT_MAX_VELOCITY_MASK;
// mChildCount = mDisplayItemCount + 2;
//
// mMiddleChild = mChildCount / 2;
//
// if(mChildCount % 2 == 0){
// mChildCount ++;
// }
//
//
// for(int i=0, n=mChildCount; i<n; i++){
// final LayoutParams lp = new LayoutParams(mItemWidth, mItemHeight);
// NumberView tv = new DateNumberView(getContext());
//
// ((TextView)tv).setText(getNumberString(i));
// ((TextView)tv).setGravity(Gravity.CENTER);
//
// addView((View)tv, lp);
// }
}
/**
* get the number from child index, use this to update the child view
* @param childIndex the index of child list
* @return if the index has no value, return ""
*/
private String getNumberString(int childIndex){
int value = mValue + childIndex - mMiddleChild;
if(value < mMinValue){
if(mWrapSelectorWheel){
value = 1 + mMaxValue - (mMinValue - value);
}
}else if(value > mMaxValue){
if(mWrapSelectorWheel){
value = mMinValue + (value - mMaxValue) - 1;
}
}
if(value >= mMinValue && value <= mMaxValue){
return mDisplayedValues == null ? formatNumber(value)
: mDisplayedValues[value - mMinValue];
}
return "";
}
private String formatNumber(int value) {
return (mFormatter != null) ? mFormatter.format(value) : String.valueOf(value);
}
/**
* Creates a new measure specification
* @param oldMeasureSpec the old measure specification
* @param newSize the max size,
* @return a new value
*/
private int makeMeasureSpec(int oldMeasureSpec, int newSize){
final int mode = MeasureSpec.getMode(oldMeasureSpec);
final int size = MeasureSpec.getSize(oldMeasureSpec);
if(mode == MeasureSpec.EXACTLY){
return oldMeasureSpec;
}else if(mode == MeasureSpec.AT_MOST){
return MeasureSpec.makeMeasureSpec(Math.min(size, newSize), MeasureSpec.EXACTLY);
}else if(mode == MeasureSpec.UNSPECIFIED){
return MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.EXACTLY);
}else{
throw new IllegalArgumentException("must use the correct parameters");
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int defX = mItemWidth;
final int defY = mDisplayItemCount * mItemHeight;
super.onMeasure(makeMeasureSpec(widthMeasureSpec, defX),
makeMeasureSpec(heightMeasureSpec, defY));
if(getMeasuredHeight() > mDisplayItemCount * mItemHeight){
mDisplayItemCount = getMeasuredHeight() / mItemHeight;
if(mDisplayItemCount % mItemHeight != 0){
mDisplayItemCount++;
}
}
mChildCount = getMeasuredHeight() / mItemHeight + 2;
if(mChildCount % 2 == 0){
mChildCount++;
}
mMiddleChild = mChildCount / 2;
if(mChildCount % 2 == 0){
mChildCount ++;
}
// for(int i=0, n=mChildCount; i<n; i++){
// final LayoutParams lp = new LayoutParams(mItemWidth, mItemHeight);
// TextView tv = new TextView(getContext());
//
// tv.setText(getNumberString(i));
// tv.setGravity(Gravity.CENTER);
// tv.setVisibility(View.INVISIBLE);
//
// addView(tv, lp);
// }
mInitialOffset = getScrollY();
//make sure the selector wheel can cycle when display item count large than (mMaxValue - mMinValue)
//确保在可显示个数大于最大最小差值的情况下可以循环滑动
setWrapSelectorWheel(mWrapSelectorWheel);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
//draw child
final int x = (getRight() - getLeft()) / 2;
final int y = (getBottom() - getTop()) / 2 + mInitialOffset;
final int curIndex = (mChildCount / 2);
for(int i=0; i<mChildCount; i++){
canvas.drawText(getNumberString(i), x, y + (i - curIndex) * mItemHeight + mChildPaint.getTextSize() / 2, mChildPaint);
}
//draw shader
if(isShaderEnable()){
//draw top
final Rect shaderTop = new Rect(0, 0 + getScrollY(), getMeasuredWidth(), mShaderHeight + getScrollY());
mShader = new LinearGradient(0, shaderTop.top, 0, shaderTop.bottom, mShaderColor, 0, Shader.TileMode.CLAMP);
mShaderPaint.setShader(mShader);
canvas.drawRect(shaderTop, mShaderPaint);
//draw bottom
final int bottomOfBottom = getMeasuredHeight() + getScrollY();
final int topOfBottom = bottomOfBottom - mShaderHeight;
final Rect shaderBottom = new Rect(0, topOfBottom, getMeasuredWidth(), bottomOfBottom);
mShader = new LinearGradient(0, bottomOfBottom, 0, topOfBottom, mShaderColor, 0, Shader.TileMode.CLAMP);
mShaderPaint.setShader(mShader);
canvas.drawRect(shaderBottom, mShaderPaint);
}
//draw selector
if(mSelector != null){
int width = mSelector.getIntrinsicWidth();
int height = mSelector.getIntrinsicHeight();
if(width < 0){
width = getWidth();
}
if(height < 0){
height = mItemHeight;
}
final int top = getScrollY() + getHeight() / 2 - height / 2;
final int bottom = top + height;
final int left = getWidth() / 2 - width / 2;
final int right = left + width;
mSelector.setBounds(left, top, right, bottom);
mSelector.draw(canvas);
}else{
// draw the top divider line
final int topOfTopDivider = getScrollY() + (getHeight() - mItemHeight) / 2;
int bottomOfTopDivider = topOfTopDivider + mSelectorlineHeight;
final Rect topRect = new Rect(0, topOfTopDivider, getWidth(), bottomOfTopDivider);
canvas.drawRect(topRect, mSelectorPaint);
// draw the bottom divider line
final int bottomOfBottomDivider = topOfTopDivider + mItemHeight;
int topOfBottomDivider = bottomOfBottomDivider - mSelectorlineHeight;
final Rect bottomRect = new Rect(0, topOfBottomDivider, getWidth(),bottomOfBottomDivider);
canvas.drawRect(bottomRect, mSelectorPaint);
}
}
public void setChlidSize(int size){
mChildPaint.setTextSize(size);
}
public void setChildColor(int color){
mChildPaint.setColor(color);
}
public void setChildPaint(Paint paint){
if(paint == null){
return;
}
mChildPaint = paint;
}
/**
* Sets the values to be displayed.
*
* @param displayedValues The displayed values.
*/
public void setDisplayedValues(String[] displayedValues) {
if (mDisplayedValues == displayedValues) {
return;
}
mDisplayedValues = displayedValues;
updateChild();
}
/**
* 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;
updateChild();
}
/**
* 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;
}
/**
* Control whether to draw shader
* @param enable
*/
public void setShaderEnable(boolean enable){
this.mShaderEnable = enable;
invalidate();
}
/**
* return the shader will can be draw
* @return
*/
public boolean isShaderEnable(){
return this.mShaderEnable;
}
/**
* set the min value on the wheel
* @param value
*/
public void setMinValue(int value){
this.mMinValue = value;
if(mValue < mMinValue){
mValue = mMinValue;
}
if(mMaxValue < mMinValue){
mMaxValue = mMinValue;
}
setWrapSelectorWheel(mWrapSelectorWheel);
}
/**
* set the max value on the wheel
* @param value
*/
public void setMaxValue(int value){
this.mMaxValue = value;
if(mValue > mMaxValue){
mValue = mMaxValue;
}
if(mMaxValue < mMinValue){
mMinValue = mMaxValue;
}
setWrapSelectorWheel(mWrapSelectorWheel);
}
/**
* set the current display value, no animation
* <br>if the given value < {@link #getMinValue()}, the program will use {@link #getMinValue()}
* <br>and the given value > {@link #getMaxValue()}, the program will use {@link #getMaxValue()}
* @param value
*/
public void setValue(int value){
if(value < mMinValue){
this.mValue = mMinValue;
}else if(value > mMaxValue){
this.mValue = mMaxValue;
}else{
this.mValue = value;
}
// onValueChange();
updateChild();
}
/**
* get the max value
* @return
*/
public int getMaxValue(){
return mMaxValue;
}
/**
*
* @return
*/
public int getMinValue(){
return mMinValue;
}
/**
*
* @return
*/
public int getValue(){
return mValue;
}
/**
* 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 the number of items shown
* on the selector wheel the selector wheel wrapping is enabled.
* </p>
* <p>
* <strong>Note:</strong> If the number of items, i.e. the range (
* {@link #getMaxValue()} - {@link #getMinValue()}) is less than
* the number of items shown on the selector wheel, the selector wheel will
* not wrap. Hence, in such a case calling this method is a NOP.
* </p>
*
* @param wrapSelectorWheel Whether to wrap.
*/
public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mDisplayItemCount;
mWrapSelectorWheel = wrappingAllowed && wrapSelectorWheel;
updateChild();
}
/**
* Gets the values to be displayed instead of string values.
*
* @return The displayed values.
*/
public String[] getDisplayedValues() {
return mDisplayedValues;
}
/**
* get the number of displayed item on screen
* @return the total number
*/
public int getDisplayItemCount(){
return mDisplayItemCount;
}
public void setDisplayItemCount(int count){
this.mDisplayItemCount = count;
invalidate();
}
/**
* 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);
}
}
/**
* get the picker scroll state
* @return
*/
public int getScrollState(){
return mScrollState;
}
/**
* handles value change, for example the value from 4 -> 4 don't handle
*/
private void onValueChange(){
if(mValue != mLastValue){
notifyValueChange(mLastValue, mValue);
mLastValue = mValue;
}
}
/**
* Notifies the listener, if registered, of a change of the value of this
* NumberPicker.
*/
private void notifyValueChange(int previous, int current) {
if (mOnValueChangeListener != null && previous != current) {
System.out.println(mScrollState);
mOnValueChangeListener.onValueChange(this, previous, current);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int dy = mScroller.getCurrY() - mLastScrollY;
scrollBy(0, -dy);
mLastScrollY = mScroller.getCurrY();
postInvalidate();
}else{
if(mScrollState == OnScrollListener.SCROLL_STATE_FLING){
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
adjustPosition();
}else if(mScrollState == OnScrollListener.SCROLL_STATE_IDLE){
onValueChange();
}
}
}
@Override
public void scrollBy(int dx, int dy) {
int scrollY = getScrollY();
scrollY += dy;
final int scrollOffset = scrollY - mInitialOffset;
if (scrollOffset >= mItemHeight || scrollOffset <= -mItemHeight) {
final int adjust = scrollOffset / mItemHeight;
updateValue(adjust);
updateChild();
scrollY = mInitialOffset + scrollOffset % mItemHeight;
playSoundEffect(SoundEffectConstants.CLICK);
}
super.scrollTo(getScrollX() + dx, scrollY);
}
@Override
public void scrollTo(int x, int y) {
// TODO Auto-generated method stub
scrollBy( x - getScrollX(), y - getScrollY());
}
/**
* update the current value
* @param adjust
*/
private void updateValue(int adjust){
mValue += adjust;
if(mValue < mMinValue){
if(mWrapSelectorWheel){
mValue = mMaxValue - (mMinValue - mValue) + 1;
}
}else if(mValue > mMaxValue){
if(mWrapSelectorWheel){
mValue = mMinValue + (mValue - mMaxValue) - 1;
}
}
}
/**
* update the child display words
*/
private void updateChild(){
// for(int i=0, n=mChildCount; i<n; i++){
// ((TextView)getChildAt(i)).setText(getNumberString(i));
// }
invalidate();
}
/**
* Recalculate the display position
*/
private void adjustPosition(){
if(mValue < mMinValue){
flingTo(mMinValue);
}else if(mValue > mMaxValue){
flingTo(mMaxValue);
}else{
final int offset = getScrollY() - mInitialOffset;
flingTo(mValue + offset / (mItemHeight / 2));
}
}
/**
* make the wheel fling to specified value
* @param value
*/
public void flingTo(int value){
final int offset = getScrollY() - mInitialOffset;
final int distance = (mValue - value) * mItemHeight + offset;
mLastScrollY = getScrollY();
mScroller.startScroll(0, mLastScrollY, 0, distance);
invalidate();
}
/**
* 根据指定的速度进行滑翔
* use specified velocity to fling
* @param velocity the Y velocity
*/
private void fling(int velocityY){
mLastScrollY = getScrollY();
mScroller.fling(0, mLastScrollY, 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int y = (int) event.getY();
final int action = event.getActionMasked();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch(action){
case MotionEvent.ACTION_DOWN:
if(mScroller.computeScrollOffset()){
mScroller.abortAnimation();
}
//不让父控件获取手势
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
final int distance = mLastY - y;
//move优化
//move optimization
if(mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL){
if(Math.abs(distance) > mTouchSlop){
onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
break;
}else
scrollBy(0, distance);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(false);
}
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
final int initialVelocity = (int) velocityTracker.getYVelocity();
if(Math.abs(initialVelocity) > mMinimumFlingVelocity){
fling(initialVelocity);
onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
}else{
adjustPosition();
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
//回收资源
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.onTouchEvent(event);
}
}