超好用的类:
在项目直接写入,可以自定义选择器,
package com.bestgo.callshow.custom_control; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.support.v4.widget.ScrollerCompat; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import com.bestgo.callshow.R; /** * Created by Carbs.Wang. * email : yeah0126@yeah.net * github : https://github.com/Carbs0126/NumberPickerView */ public class NumberPickerView extends View { // default text color of not selected item private static final int DEFAULT_TEXT_COLOR_NORMAL = 0XFF333333; // default text color of selected item private static final int DEFAULT_TEXT_COLOR_SELECTED = 0XFFF56313; // default text size of normal item private static final int DEFAULT_TEXT_SIZE_NORMAL_SP = 14; // default text size of selected item private static final int DEFAULT_TEXT_SIZE_SELECTED_SP = 16; // default text size of hint text, the middle item's right text private static final int DEFAULT_TEXT_SIZE_HINT_SP = 14; // distance between selected text and hint text private static final int DEFAULT_MARGIN_START_OF_HINT_DP = 8; // distance between hint text and right of this view, used in wrap_content mode private static final int DEFAULT_MARGIN_END_OF_HINT_DP = 8; // default divider's color private static final int DEFAULT_DIVIDER_COLOR = 0XFFCDCDC1; // default divider's height private static final int DEFAULT_DIVIDER_HEIGHT = 2; // default divider's margin to the left & right of this view private static final int DEFAULT_DIVIDER_MARGIN_HORIZONTAL = 0; // default shown items' count, now we display 3 items, the 2nd one is selected private static final int DEFAULT_SHOW_COUNT = 3; // default items' horizontal padding, left padding and right padding are both 5dp, // only used in wrap_content mode private static final int DEFAULT_ITEM_PADDING_DP_H = 5; // default items' vertical padding, top padding and bottom padding are both 2dp, // only used in wrap_content mode private static final int DEFAULT_ITEM_PADDING_DP_V = 2; // message's what argument to refresh current state, used by mHandler private static final int HANDLER_WHAT_REFRESH = 1; // message's what argument to respond value changed event, used by mHandler private static final int HANDLER_WHAT_LISTENER_VALUE_CHANGED = 2; // message's what argument to request layout, used by mHandlerInMainThread private static final int HANDLER_WHAT_REQUEST_LAYOUT = 3; // interval time to scroll the distance of one item's height private static final int HANDLER_INTERVAL_REFRESH = 32; //millisecond // in millisecond unit, default duration of scrolling an item' distance private static final int DEFAULT_INTERVAL_REVISE_DURATION = 300; // max and min durations when scrolling from one value to another private static final int DEFAULT_MIN_SCROLL_BY_INDEX_DURATION = DEFAULT_INTERVAL_REVISE_DURATION * 1; private static final int DEFAULT_MAX_SCROLL_BY_INDEX_DURATION = DEFAULT_INTERVAL_REVISE_DURATION * 2; private static final String TEXT_ELLIPSIZE_START = "start" ; private static final String TEXT_ELLIPSIZE_MIDDLE = "middle" ; private static final String TEXT_ELLIPSIZE_END = "end" ; private static final boolean DEFAULT_SHOW_DIVIDER = true; private static final boolean DEFAULT_WRAP_SELECTOR_WHEEL = true; private static final boolean DEFAULT_CURRENT_ITEM_INDEX_EFFECT = false; private static final boolean DEFAULT_RESPOND_CHANGE_ON_DETACH = false; private static final boolean DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD = true; private int mTextColorNormal = DEFAULT_TEXT_COLOR_NORMAL; private int mTextColorSelected = DEFAULT_TEXT_COLOR_SELECTED; private int mTextColorHint = DEFAULT_TEXT_COLOR_SELECTED; private int mTextSizeNormal = 0; private int mTextSizeSelected = 0; private int mTextSizeHint = 0; private int mWidthOfHintText = 0; private int mWidthOfAlterHint = 0; private int mMarginStartOfHint = 0; private int mMarginEndOfHint = 0; private int mItemPaddingVertical = 0; private int mItemPaddingHorizontal = 0; private int mDividerColor = DEFAULT_DIVIDER_COLOR; private int mDividerHeight = DEFAULT_DIVIDER_HEIGHT; private int mDividerMarginL = DEFAULT_DIVIDER_MARGIN_HORIZONTAL; private int mDividerMarginR = DEFAULT_DIVIDER_MARGIN_HORIZONTAL; private int mShowCount = DEFAULT_SHOW_COUNT; private int mDividerIndex0 = 0; private int mDividerIndex1 = 0; private int mMinShowIndex = -1; private int mMaxShowIndex = -1; //compat for android.widget.NumberPicker private int mMinValue = 0; //compat for android.widget.NumberPicker private int mMaxValue = 0; private int mMaxWidthOfDisplayedValues = 0; private int mMaxHeightOfDisplayedValues = 0; private int mMaxWidthOfAlterArrayWithMeasureHint = 0; private int mMaxWidthOfAlterArrayWithoutMeasureHint = 0; private int mPrevPickedIndex = 0; private int mMiniVelocityFling = 150; private int mScaledTouchSlop = 8; private String mHintText; private String mTextEllipsize; private String mEmptyItemHint; private String mAlterHint; //friction used by scroller when fling private float mFriction = 1f; private float mTextSizeNormalCenterYOffset = 0f; private float mTextSizeSelectedCenterYOffset = 0f; private float mTextSizeHintCenterYOffset = 0f; //true to show the two dividers private boolean mShowDivider = DEFAULT_SHOW_DIVIDER; //true to wrap the displayed values private boolean mWrapSelectorWheel = DEFAULT_WRAP_SELECTOR_WHEEL; //true to set to the current position, false set position to 0 private boolean mCurrentItemIndexEffect = DEFAULT_CURRENT_ITEM_INDEX_EFFECT; //true if NumberPickerView has initialized private boolean mHasInit = false; // if displayed values' number is less than show count, then this value will be false. private boolean mWrapSelectorWheelCheck = true; // if you want you set to linear mode from wrap mode when scrolling, then this value will be true. private boolean mPendingWrapToLinear = false; // if this view is used in same dialog or PopupWindow more than once, and there are several // NumberPickerViews linked, such as Gregorian Calendar with MonthPicker and DayPicker linked, // set mRespondChangeWhenDetach true to respond onValueChanged callbacks if this view is scrolling // when detach from window, but this solution is unlovely and may cause NullPointerException // (even i haven't found this NullPointerException), // so I highly recommend that every time setting up a reusable dialog with a NumberPickerView in it, // please initialize NumberPickerView's data, and in this way, you can set mRespondChangeWhenDetach false. private boolean mRespondChangeOnDetach = DEFAULT_RESPOND_CHANGE_ON_DETACH; // this is to set which thread to respond onChange... listeners including // OnValueChangeListener, OnValueChangeListenerRelativeToRaw and OnScrollListener when view is // scrolling or starts to scroll or stops scrolling. private boolean mRespondChangeInMainThread = DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD; private ScrollerCompat mScroller; private VelocityTracker mVelocityTracker; private Paint mPaintDivider = new Paint(); private TextPaint mPaintText = new TextPaint(); private Paint mPaintHint = new Paint(); private String[] mDisplayedValues; private CharSequence[] mAlterTextArrayWithMeasureHint; private CharSequence[] mAlterTextArrayWithoutMeasureHint; private HandlerThread mHandlerThread; private Handler mHandlerInNewThread; private Handler mHandlerInMainThread; // compatible for NumberPicker public interface OnValueChangeListener{ void onValueChange(NumberPickerView picker, int oldVal, int newVal); } public interface OnValueChangeListenerRelativeToRaw{ void onValueChangeRelativeToRaw(NumberPickerView picker, int oldPickedIndex, int newPickedIndex, String[] displayedValues); } public interface OnValueChangeListenerInScrolling{ void onValueChangeInScrolling(NumberPickerView picker, int oldVal, int newVal); } // compatible for NumberPicker public interface OnScrollListener { int SCROLL_STATE_IDLE = 0; int SCROLL_STATE_TOUCH_SCROLL = 1; int SCROLL_STATE_FLING = 2; void onScrollStateChange(NumberPickerView view, int scrollState); } private OnValueChangeListenerRelativeToRaw mOnValueChangeListenerRaw; private OnValueChangeListener mOnValueChangeListener; //compatible for NumberPicker private OnScrollListener mOnScrollListener; //compatible for NumberPicker private OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling; //response onValueChanged in scrolling // The current scroll state of the NumberPickerView. private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; public NumberPickerView(Context context) { super(context); init(context); } public NumberPickerView(Context context, AttributeSet attrs) { super(context, attrs); initAttr(context, attrs); init(context); } public NumberPickerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttr(context, attrs); init(context); } private void initAttr(Context context, AttributeSet attrs){ if (attrs == null) { return; } TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberPickerView); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); if(attr == R.styleable.NumberPickerView_npv_ShowCount){ mShowCount = a.getInt(attr, DEFAULT_SHOW_COUNT); }else if(attr == R.styleable.NumberPickerView_npv_DividerColor){ mDividerColor = a.getColor(attr, DEFAULT_DIVIDER_COLOR); }else if(attr == R.styleable.NumberPickerView_npv_DividerHeight){ mDividerHeight = a.getDimensionPixelSize(attr, DEFAULT_DIVIDER_HEIGHT); }else if(attr == R.styleable.NumberPickerView_npv_DividerMarginLeft){ mDividerMarginL = a.getDimensionPixelSize(attr, DEFAULT_DIVIDER_MARGIN_HORIZONTAL); }else if(attr == R.styleable.NumberPickerView_npv_DividerMarginRight){ mDividerMarginR = a.getDimensionPixelSize(attr, DEFAULT_DIVIDER_MARGIN_HORIZONTAL); }else if(attr == R.styleable.NumberPickerView_npv_TextArray){ mDisplayedValues = convertCharSequenceArrayToStringArray(a.getTextArray(attr)); }else if(attr == R.styleable.NumberPickerView_npv_TextColorNormal){ mTextColorNormal = a.getColor(attr, DEFAULT_TEXT_COLOR_NORMAL); }else if(attr == R.styleable.NumberPickerView_npv_TextColorSelected){ mTextColorSelected = a.getColor(attr, DEFAULT_TEXT_COLOR_SELECTED); }else if(attr == R.styleable.NumberPickerView_npv_TextColorHint){ mTextColorHint = a.getColor(attr, DEFAULT_TEXT_COLOR_SELECTED); }else if(attr == R.styleable.NumberPickerView_npv_TextSizeNormal){ mTextSizeNormal = a.getDimensionPixelSize(attr, sp2px(context, DEFAULT_TEXT_SIZE_NORMAL_SP)); }else if(attr == R.styleable.NumberPickerView_npv_TextSizeSelected){ mTextSizeSelected = a.getDimensionPixelSize(attr, sp2px(context, DEFAULT_TEXT_SIZE_SELECTED_SP)); }else if(attr == R.styleable.NumberPickerView_npv_TextSizeHint){ mTextSizeHint = a.getDimensionPixelSize(attr, sp2px(context, DEFAULT_TEXT_SIZE_HINT_SP)); }else if(attr == R.styleable.NumberPickerView_npv_MinValue){ mMinShowIndex = a.getInteger(attr, 0); }else if(attr == R.styleable.NumberPickerView_npv_MaxValue){ mMaxShowIndex = a.getInteger(attr, 0); }else if(attr == R.styleable.NumberPickerView_npv_WrapSelectorWheel){ mWrapSelectorWheel = a.getBoolean(attr, DEFAULT_WRAP_SELECTOR_WHEEL); }else if(attr == R.styleable.NumberPickerView_npv_ShowDivider){ mShowDivider = a.getBoolean(attr, DEFAULT_SHOW_DIVIDER); }else if(attr == R.styleable.NumberPickerView_npv_HintText){ mHintText = a.getString(attr); }else if(attr == R.styleable.NumberPickerView_npv_AlternativeHint){ mAlterHint = a.getString(attr); }else if(attr == R.styleable.NumberPickerView_npv_EmptyItemHint){ mEmptyItemHint = a.getString(attr); }else if(attr == R.styleable.NumberPickerView_npv_MarginStartOfHint){ mMarginStartOfHint = a.getDimensionPixelSize(attr, dp2px(context, DEFAULT_MARGIN_START_OF_HINT_DP)); }else if(attr == R.styleable.NumberPickerView_npv_MarginEndOfHint){ mMarginEndOfHint = a.getDimensionPixelSize(attr, dp2px(context, DEFAULT_MARGIN_END_OF_HINT_DP)); }else if(attr == R.styleable.NumberPickerView_npv_ItemPaddingVertical){ mItemPaddingVertical = a.getDimensionPixelSize(attr, dp2px(context, DEFAULT_ITEM_PADDING_DP_V)); }else if(attr == R.styleable.NumberPickerView_npv_ItemPaddingHorizontal){ mItemPaddingHorizontal = a.getDimensionPixelSize(attr, dp2px(context, DEFAULT_ITEM_PADDING_DP_H)); }else if(attr == R.styleable.NumberPickerView_npv_AlternativeTextArrayWithMeasureHint){ mAlterTextArrayWithMeasureHint = a.getTextArray(attr); }else if(attr == R.styleable.NumberPickerView_npv_AlternativeTextArrayWithoutMeasureHint){ mAlterTextArrayWithoutMeasureHint = a.getTextArray(attr); }else if(attr == R.styleable.NumberPickerView_npv_RespondChangeOnDetached){ mRespondChangeOnDetach = a.getBoolean(attr, DEFAULT_RESPOND_CHANGE_ON_DETACH); }else if(attr == R.styleable.NumberPickerView_npv_RespondChangeInMainThread){ mRespondChangeInMainThread = a.getBoolean(attr, DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD); }else if (attr == R.styleable.NumberPickerView_npv_TextEllipsize) { mTextEllipsize = a.getString(attr); } } a.recycle(); } private void init(Context context){ mScroller = ScrollerCompat.create(context); mMiniVelocityFling = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity(); mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); if(mTextSizeNormal == 0) { mTextSizeNormal = sp2px(context, DEFAULT_TEXT_SIZE_NORMAL_SP); } if(mTextSizeSelected == 0) { mTextSizeSelected = sp2px(context, DEFAULT_TEXT_SIZE_SELECTED_SP); } if(mTextSizeHint == 0) { mTextSizeHint = sp2px(context, DEFAULT_TEXT_SIZE_HINT_SP); } if(mMarginStartOfHint == 0) { mMarginStartOfHint = dp2px(context, DEFAULT_MARGIN_START_OF_HINT_DP); } if(mMarginEndOfHint == 0) { mMarginEndOfHint = dp2px(context, DEFAULT_MARGIN_END_OF_HINT_DP); } mPaintDivider.setColor(mDividerColor); mPaintDivider.setAntiAlias(true); mPaintDivider.setStyle(Paint.Style.FILL_AND_STROKE); mPaintDivider.setStrokeWidth(mDividerHeight); mPaintText.setColor(mTextColorNormal); mPaintText.setAntiAlias(true); mPaintText.setTextAlign(Paint.Align.CENTER); mPaintHint.setColor(mTextColorHint); mPaintHint.setAntiAlias(true); mPaintHint.setTextAlign(Paint.Align.CENTER); mPaintHint.setTextSize(mTextSizeHint); if(mShowCount % 2 == 0){ mShowCount++; } if(mMinShowIndex == -1 || mMaxShowIndex == -1){ updateValueForInit(); } initHandler(); } private void initHandler(){ mHandlerThread = new HandlerThread( "HandlerThread-For-Refreshing" ); mHandlerThread.start(); mHandlerInNewThread = new Handler(mHandlerThread.getLooper()){ @Override public void handleMessage(Message msg) { switch(msg.what){ case HANDLER_WHAT_REFRESH: if(!mScroller.isFinished()){ if(mScrollState == OnScrollListener.SCROLL_STATE_IDLE){ onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH, 0, 0, msg.obj), HANDLER_INTERVAL_REFRESH); }else{ int duration = 0; int willPickIndex; //if scroller finished(not scrolling), then adjust the position if(mCurrDrawFirstItemY != 0){ //need to adjust if(mScrollState == OnScrollListener.SCROLL_STATE_IDLE){ onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } if(mCurrDrawFirstItemY < (-mItemHeight/2)){ //adjust to scroll upward duration = (int)((float)DEFAULT_INTERVAL_REVISE_DURATION * (mItemHeight + mCurrDrawFirstItemY) / mItemHeight); mScroller.startScroll(0, mCurrDrawGlobalY, 0, mItemHeight + mCurrDrawFirstItemY, duration * 3); willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mItemHeight + mCurrDrawFirstItemY); }else{ //adjust to scroll downward duration = (int)((float)DEFAULT_INTERVAL_REVISE_DURATION * (-mCurrDrawFirstItemY) / mItemHeight); mScroller.startScroll(0, mCurrDrawGlobalY, 0, mCurrDrawFirstItemY, duration * 3); willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mCurrDrawFirstItemY); } postInvalidate(); }else{ onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); //get the index which will be selected willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY); } Message changeMsg = getMsg(HANDLER_WHAT_LISTENER_VALUE_CHANGED, mPrevPickedIndex, willPickIndex, msg.obj); if(mRespondChangeInMainThread){ mHandlerInMainThread.sendMessageDelayed(changeMsg, duration * 2); }else{ mHandlerInNewThread.sendMessageDelayed(changeMsg, duration * 2); } } break; case HANDLER_WHAT_LISTENER_VALUE_CHANGED: respondPickedValueChanged(msg.arg1, msg.arg2, msg.obj); break; } } }; mHandlerInMainThread = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what){ case HANDLER_WHAT_REQUEST_LAYOUT: requestLayout(); break; case HANDLER_WHAT_LISTENER_VALUE_CHANGED: respondPickedValueChanged(msg.arg1, msg.arg2, msg.obj); break; } } }; } private int mInScrollingPickedOldValue; private int mInScrollingPickedNewValue; private void respondPickedValueChangedInScrolling(int oldVal, int newVal) { mOnValueChangeListenerInScrolling.onValueChangeInScrolling(this, oldVal, newVal); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); updateMaxWHOfDisplayedValues(false); setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mViewWidth = w; mViewHeight = h; mItemHeight = mViewHeight / mShowCount; mViewCenterX = ((float)(mViewWidth + getPaddingLeft() - getPaddingRight()))/2; int defaultValue = 0; if(getOneRecycleSize() > 1){ if(mHasInit) { defaultValue = getValue() - mMinValue; }else if(mCurrentItemIndexEffect) { defaultValue = mCurrDrawFirstItemIndex + (mShowCount - 1) / 2; }else{ defaultValue = 0; } } correctPositionByDefaultValue(defaultValue, mWrapSelectorWheel && mWrapSelectorWheelCheck); updateFontAttr(); updateNotWrapYLimit(); updateDividerAttr(); mHasInit = true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if(mHandlerThread == null || !mHandlerThread.isAlive()) { initHandler(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mHandlerThread.quit(); //These codes are for dialog or PopupWindow which will be used for more than once. //Not an elegant solution, if you have any good idea, please let me know, thank you. if(mItemHeight == 0) return; if(!mScroller.isFinished()){ mScroller.abortAnimation(); mCurrDrawGlobalY = mScroller.getCurrY(); calculateFirstItemParameterByGlobalY(); if(mCurrDrawFirstItemY != 0){ if(mCurrDrawFirstItemY < (-mItemHeight/2)){ mCurrDrawGlobalY = mCurrDrawGlobalY + mItemHeight + mCurrDrawFirstItemY; }else{ mCurrDrawGlobalY = mCurrDrawGlobalY + mCurrDrawFirstItemY; } calculateFirstItemParameterByGlobalY(); } onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } // see the comments on mRespondChangeOnDetach, if mRespondChangeOnDetach is false, // please initialize NumberPickerView's data every time setting up NumberPickerView, // set the demo of GregorianLunarCalendar int currPickedIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY); if(currPickedIndex != mPrevPickedIndex && mRespondChangeOnDetach){ try { if (mOnValueChangeListener != null) { mOnValueChangeListener.onValueChange(NumberPickerView.this, mPrevPickedIndex + mMinValue, currPickedIndex + mMinValue); } if (mOnValueChangeListenerRaw != null) { mOnValueChangeListenerRaw.onValueChangeRelativeToRaw(NumberPickerView.this, mPrevPickedIndex, currPickedIndex, mDisplayedValues); } }catch (Exception e){ e.printStackTrace(); } } mPrevPickedIndex = currPickedIndex; } public int getOneRecycleSize(){ return mMaxShowIndex - mMinShowIndex + 1; } public int getRawContentSize(){ if(mDisplayedValues != null) return mDisplayedValues.length; return 0; } public void setDisplayedValuesAndPickedIndex(String[] newDisplayedValues, int pickedIndex, boolean needRefresh){ stopScrolling(); if(newDisplayedValues == null){ throw new IllegalArgumentException( "newDisplayedValues should not be null." ); } if(pickedIndex < 0){ throw new IllegalArgumentException( "pickedIndex should not be negative, now pickedIndex is " + pickedIndex); } updateContent(newDisplayedValues); updateMaxWHOfDisplayedValues(true); updateNotWrapYLimit(); updateValue(); mPrevPickedIndex = pickedIndex + mMinShowIndex; correctPositionByDefaultValue(pickedIndex, mWrapSelectorWheel && mWrapSelectorWheelCheck); if(needRefresh){ mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0); postInvalidate(); } } public void setDisplayedValues(String[] newDisplayedValues, boolean needRefresh){ setDisplayedValuesAndPickedIndex(newDisplayedValues, 0, needRefresh); } public void setDisplayedValues(String[] newDisplayedValues){ stopRefreshing(); stopScrolling(); if(newDisplayedValues == null){ throw new IllegalArgumentException( "newDisplayedValues should not be null." ); } if(mMaxValue - mMinValue + 1 > newDisplayedValues.length){ throw new IllegalArgumentException( "mMaxValue - mMinValue + 1 should not be greater than mDisplayedValues.length, now " + "((mMaxValue - mMinValue + 1) is " + (mMaxValue - mMinValue + 1) + " newDisplayedValues.length is " + newDisplayedValues.length + ", you need to set MaxValue and MinValue before setDisplayedValues(String[])" ); } updateContent(newDisplayedValues); updateMaxWHOfDisplayedValues(true); mPrevPickedIndex = 0 + mMinShowIndex; correctPositionByDefaultValue(0, mWrapSelectorWheel && mWrapSelectorWheelCheck); postInvalidate(); mHandlerInMainThread.sendEmptyMessage(HANDLER_WHAT_REQUEST_LAYOUT); } /** * Gets the values to be displayed instead of string values. * @return The displayed values. */ public String[] getDisplayedValues() { return mDisplayedValues; } public void setWrapSelectorWheel(boolean wrapSelectorWheel){ if(mWrapSelectorWheel != wrapSelectorWheel) { if(!wrapSelectorWheel) { if(mScrollState == OnScrollListener.SCROLL_STATE_IDLE){ internalSetWrapToLinear(); }else{ mPendingWrapToLinear = true; } }else{ mWrapSelectorWheel = wrapSelectorWheel; updateWrapStateByContent(); postInvalidate(); } } } /** * get the "fromValue" by using getValue(), if your picker's minValue is not 0, * make sure you can get the accurate value by getValue(), or you can use * smoothScrollToValue(int fromValue, int toValue, boolean needRespond) * @param toValue the value you want picker to scroll to */ public void smoothScrollToValue(int toValue){ smoothScrollToValue(getValue(), toValue, true); } /** * get the "fromValue" by using getValue(), if your picker's minValue is not 0, * make sure you can get the accurate value by getValue(), or you can use * smoothScrollToValue(int fromValue, int toValue, boolean needRespond) * @param toValue the value you want picker to scroll to * @param needRespond set if you want picker to respond onValueChange listener */ public void smoothScrollToValue(int toValue, boolean needRespond){ smoothScrollToValue(getValue(), toValue, needRespond); } public void smoothScrollToValue(int fromValue, int toValue){ smoothScrollToValue(fromValue, toValue, true); } /** * * @param fromValue need to set the fromValue, can be greater than mMaxValue or less than mMinValue * @param toValue the value you want picker to scroll to * @param needRespond need Respond to the ValueChange callback When Scrolling, default is false */ public void smoothScrollToValue(int fromValue, int toValue, boolean needRespond){ int deltaIndex; fromValue = refineValueByLimit(fromValue, mMinValue, mMaxValue, mWrapSelectorWheel && mWrapSelectorWheelCheck); toValue = refineValueByLimit(toValue, mMinValue, mMaxValue, mWrapSelectorWheel && mWrapSelectorWheelCheck); if(mWrapSelectorWheel && mWrapSelectorWheelCheck) { deltaIndex = toValue - fromValue; int halfOneRecycleSize = getOneRecycleSize() / 2; if(deltaIndex < -halfOneRecycleSize || halfOneRecycleSize < deltaIndex ){ deltaIndex = deltaIndex > 0 ? deltaIndex - getOneRecycleSize() : deltaIndex + getOneRecycleSize(); } }else{ deltaIndex = toValue - fromValue; } setValue(fromValue); if(fromValue == toValue) return; scrollByIndexSmoothly(deltaIndex, needRespond); } /** * simplify the "setDisplayedValue() + setMinValue() + setMaxValue()" process, * default minValue is 0, and make sure you do NOT change the minValue. * @param display new values to be displayed */ public void refreshByNewDisplayedValues(String[] display) { int minValue = getMinValue(); int oldMaxValue = getMaxValue(); int oldSpan = oldMaxValue - minValue + 1; int newMaxValue = display.length - 1; int newSpan = newMaxValue - minValue + 1; if (newSpan > oldSpan) { setDisplayedValues(display); setMaxValue(newMaxValue); } else { setMaxValue(newMaxValue); setDisplayedValues(display); } } /** * used by handlers to respond onchange callbacks * @param oldVal prevPicked value * @param newVal currPicked value * @param respondChange if want to respond onchange callbacks */ private void respondPickedValueChanged(int oldVal, int newVal, Object respondChange){ onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); if(oldVal != newVal){ if(respondChange == null || !(respondChange instanceof Boolean) || (Boolean) respondChange) { if(mOnValueChangeListener != null) { mOnValueChangeListener.onValueChange(NumberPickerView.this, oldVal + mMinValue, newVal + mMinValue); } if(mOnValueChangeListenerRaw != null){ mOnValueChangeListenerRaw.onValueChangeRelativeToRaw(NumberPickerView.this, oldVal, newVal, mDisplayedValues); } } } mPrevPickedIndex = newVal; if(mPendingWrapToLinear){ mPendingWrapToLinear = false; internalSetWrapToLinear(); } } private void scrollByIndexSmoothly(int deltaIndex){ scrollByIndexSmoothly(deltaIndex, true); } /** * * @param deltaIndex the delta index it will scroll by * @param needRespond need Respond to the ValueChange callback When Scrolling, default is false */ private void scrollByIndexSmoothly(int deltaIndex, boolean needRespond){ if(!(mWrapSelectorWheel && mWrapSelectorWheelCheck)){ int willPickRawIndex = getPickedIndexRelativeToRaw(); if(willPickRawIndex + deltaIndex > mMaxShowIndex){ deltaIndex = mMaxShowIndex - willPickRawIndex; }else if(willPickRawIndex + deltaIndex < mMinShowIndex){ deltaIndex = mMinShowIndex - willPickRawIndex; } } int duration; int dy; if(mCurrDrawFirstItemY < (-mItemHeight/2)){ //scroll upwards for a distance of less than mItemHeight dy = mItemHeight + mCurrDrawFirstItemY; duration = (int)((float)DEFAULT_INTERVAL_REVISE_DURATION * (mItemHeight + mCurrDrawFirstItemY) / mItemHeight); if(deltaIndex < 0){ duration = -duration - deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION; }else{ duration = duration + deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION; } }else{ //scroll downwards for a distance of less than mItemHeight dy = mCurrDrawFirstItemY; duration = (int)((float)DEFAULT_INTERVAL_REVISE_DURATION * (-mCurrDrawFirstItemY) / mItemHeight); if(deltaIndex < 0){ duration = duration - deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION; }else{ duration = duration + deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION; } } dy = dy + deltaIndex * mItemHeight; if(duration < DEFAULT_MIN_SCROLL_BY_INDEX_DURATION) duration = DEFAULT_MIN_SCROLL_BY_INDEX_DURATION; if(duration > DEFAULT_MAX_SCROLL_BY_INDEX_DURATION) duration = DEFAULT_MAX_SCROLL_BY_INDEX_DURATION; mScroller.startScroll(0, mCurrDrawGlobalY, 0, dy, duration); if(needRespond){ mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), duration / 4); }else{ mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH, 0, 0, new Boolean(needRespond)), duration / 4); } postInvalidate(); } public int getMinValue(){ return mMinValue; } public int getMaxValue(){ return mMaxValue; } public void setMinValue(int minValue){ mMinValue = minValue; mMinShowIndex = 0; updateNotWrapYLimit(); } //compatible for android.widget.NumberPicker public void setMaxValue(int maxValue){ if(mDisplayedValues == null){ throw new NullPointerException( "mDisplayedValues should not be null" ); } if(maxValue - mMinValue + 1 > mDisplayedValues.length){ throw new IllegalArgumentException( "(maxValue - mMinValue + 1) should not be greater than mDisplayedValues.length now " + " (maxValue - mMinValue + 1) is " + (maxValue - mMinValue + 1) + " and mDisplayedValues.length is " + mDisplayedValues.length); } mMaxValue = maxValue; mMaxShowIndex = mMaxValue - mMinValue + mMinShowIndex; setMinAndMaxShowIndex(mMinShowIndex, mMaxShowIndex); updateNotWrapYLimit(); } //compatible for android.widget.NumberPicker public void setValue(int value){ if(value < mMinValue){ throw new IllegalArgumentException( "should not set a value less than mMinValue, value is " + value); } if(value > mMaxValue){ throw new IllegalArgumentException( "should not set a value greater than mMaxValue, value is " + value); } setPickedIndexRelativeToRaw(value - mMinValue); } //compatible for android.widget.NumberPicker public int getValue(){ return getPickedIndexRelativeToRaw() + mMinValue; } public String getContentByCurrValue(){ return mDisplayedValues[getValue() - mMinValue]; } public boolean getWrapSelectorWheel(){ return mWrapSelectorWheel; } public boolean getWrapSelectorWheelAbsolutely(){ return mWrapSelectorWheel && mWrapSelectorWheelCheck; } public void setHintText(String hintText){ if(isStringEqual(mHintText, hintText)) return; mHintText = hintText; mTextSizeHintCenterYOffset = getTextCenterYOffset(mPaintHint.getFontMetrics()); mWidthOfHintText = getTextWidth(mHintText, mPaintHint); mHandlerInMainThread.sendEmptyMessage(HANDLER_WHAT_REQUEST_LAYOUT); } public void setPickedIndexRelativeToMin(int pickedIndexToMin){ if(0 <= pickedIndexToMin && pickedIndexToMin < getOneRecycleSize()){ mPrevPickedIndex = pickedIndexToMin + mMinShowIndex; correctPositionByDefaultValue(pickedIndexToMin, mWrapSelectorWheel && mWrapSelectorWheelCheck); postInvalidate(); } } public void setNormalTextColor(int normalTextColor){ if(mTextColorNormal == normalTextColor) return; mTextColorNormal = normalTextColor; postInvalidate(); } public void setSelectedTextColor(int selectedTextColor){ if(mTextColorSelected == selectedTextColor) return; mTextColorSelected = selectedTextColor; postInvalidate(); } public void setHintTextColor(int hintTextColor){ if(mTextColorHint == hintTextColor) return; mTextColorHint = hintTextColor; mPaintHint.setColor(mTextColorHint); postInvalidate(); } public void setDividerColor(int dividerColor){ if(mDividerColor == dividerColor) return; mDividerColor = dividerColor; mPaintDivider.setColor(mDividerColor); postInvalidate(); } public void setPickedIndexRelativeToRaw(int pickedIndexToRaw){ if(mMinShowIndex > -1){ if(mMinShowIndex <= pickedIndexToRaw && pickedIndexToRaw <= mMaxShowIndex){ mPrevPickedIndex = pickedIndexToRaw; correctPositionByDefaultValue(pickedIndexToRaw - mMinShowIndex, mWrapSelectorWheel && mWrapSelectorWheelCheck); postInvalidate(); } } } public int getPickedIndexRelativeToRaw(){ int willPickIndex; if(mCurrDrawFirstItemY != 0){ if(mCurrDrawFirstItemY < (-mItemHeight/2)){ willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mItemHeight + mCurrDrawFirstItemY); }else{ willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mCurrDrawFirstItemY); } }else{ willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY); } return willPickIndex; } public void setMinAndMaxShowIndex(int minShowIndex, int maxShowIndex){ setMinAndMaxShowIndex(minShowIndex, maxShowIndex, true); } public void setMinAndMaxShowIndex(int minShowIndex, int maxShowIndex, boolean needRefresh){ if(minShowIndex > maxShowIndex){ throw new IllegalArgumentException( "minShowIndex should be less than maxShowIndex, minShowIndex is " + minShowIndex + ", maxShowIndex is " + maxShowIndex + "." ); } if(mDisplayedValues == null){ throw new IllegalArgumentException( "mDisplayedValues should not be null, you need to set mDisplayedValues first." ); } else { if(minShowIndex < 0){ throw new IllegalArgumentException( "minShowIndex should not be less than 0, now minShowIndex is " + minShowIndex); } else if(minShowIndex > mDisplayedValues.length - 1){ throw new IllegalArgumentException( "minShowIndex should not be greater than (mDisplayedValues.length - 1), now " + "(mDisplayedValues.length - 1) is " + (mDisplayedValues.length - 1) + " minShowIndex is " + minShowIndex); } if(maxShowIndex < 0){ throw new IllegalArgumentException( "maxShowIndex should not be less than 0, now maxShowIndex is " + maxShowIndex); } else if(maxShowIndex > mDisplayedValues.length - 1){ throw new IllegalArgumentException( "maxShowIndex should not be greater than (mDisplayedValues.length - 1), now " + "(mDisplayedValues.length - 1) is " + (mDisplayedValues.length - 1) + " maxShowIndex is " + maxShowIndex); } } mMinShowIndex = minShowIndex; mMaxShowIndex = maxShowIndex; if(needRefresh){ mPrevPickedIndex = 0 + mMinShowIndex; correctPositionByDefaultValue(0, mWrapSelectorWheel && mWrapSelectorWheelCheck); postInvalidate(); } } /** * set the friction of scroller, it will effect the scroller's acceleration when fling * @param friction default is ViewConfiguration.get(mContext).getScrollFriction() * if setFriction(2 * ViewConfiguration.get(mContext).getScrollFriction()), * the friction will be twice as much as before */ public void setFriction(float friction){ if(friction <= 0) throw new IllegalArgumentException( "you should set a a positive float friction, now friction is " + friction); mFriction = ViewConfiguration.get(getContext()).getScrollFriction() / friction; } //compatible for NumberPicker private void onScrollStateChange(int scrollState) { if (mScrollState == scrollState) { return; } mScrollState = scrollState; if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChange(this, scrollState); } } //compatible for NumberPicker public void setOnScrollListener(OnScrollListener listener){ mOnScrollListener = listener; } //compatible for NumberPicker public void setOnValueChangedListener(OnValueChangeListener listener){ mOnValueChangeListener = listener; } public void setOnValueChangedListenerRelativeToRaw(OnValueChangeListenerRelativeToRaw listener){ mOnValueChangeListenerRaw = listener; } public void setOnValueChangeListenerInScrolling(OnValueChangeListenerInScrolling listener){ mOnValueChangeListenerInScrolling = listener; } public void setContentTextTypeface(Typeface typeface){ mPaintText.setTypeface(typeface); } public void setHintTextTypeface(Typeface typeface){ mPaintHint.setTypeface(typeface); } //return index relative to mDisplayedValues from 0. private int getWillPickIndexByGlobalY(int globalY){ if(mItemHeight == 0) return 0; int willPickIndex = globalY / mItemHeight + mShowCount / 2; int index = getIndexByRawIndex(willPickIndex, getOneRecycleSize(), mWrapSelectorWheel && mWrapSelectorWheelCheck); if(0 <= index && index < getOneRecycleSize()){ return index + mMinShowIndex; }else{ throw new IllegalArgumentException( "getWillPickIndexByGlobalY illegal index : " + index + " getOneRecycleSize() : " + getOneRecycleSize() + " mWrapSelectorWheel : " + mWrapSelectorWheel); } } private int getIndexByRawIndex(int index, int size, boolean wrap){ if(size <= 0) return 0; if(wrap){ index = index % size; if(index < 0){ index = index + size; } return index; }else{ return index; } } private void internalSetWrapToLinear(){ int rawIndex = getPickedIndexRelativeToRaw(); correctPositionByDefaultValue(rawIndex - mMinShowIndex, false); mWrapSelectorWheel = false; postInvalidate(); } private void updateDividerAttr(){ mDividerIndex0 = mShowCount / 2; mDividerIndex1 = mDividerIndex0 + 1; dividerY0 = mDividerIndex0 * mViewHeight / mShowCount; dividerY1 = mDividerIndex1 * mViewHeight / mShowCount; if(mDividerMarginL < 0) mDividerMarginL = 0; if(mDividerMarginR < 0) mDividerMarginR = 0; if(mDividerMarginL + mDividerMarginR == 0) return; if(getPaddingLeft() + mDividerMarginL >= mViewWidth - getPaddingRight() - mDividerMarginR){ int surplusMargin = getPaddingLeft() + mDividerMarginL + getPaddingRight() + mDividerMarginR - mViewWidth; mDividerMarginL = (int)(mDividerMarginL - (float)surplusMargin * mDividerMarginL/(mDividerMarginL + mDividerMarginR)); mDividerMarginR = (int)(mDividerMarginR - (float)surplusMargin * mDividerMarginR/(mDividerMarginL + mDividerMarginR)); } } private int mNotWrapLimitYTop; private int mNotWrapLimitYBottom; private void updateFontAttr(){ if(mTextSizeNormal > mItemHeight) mTextSizeNormal = mItemHeight; if(mTextSizeSelected > mItemHeight) mTextSizeSelected = mItemHeight; if(mPaintHint == null){ throw new IllegalArgumentException( "mPaintHint should not be null." ); } mPaintHint.setTextSize(mTextSizeHint); mTextSizeHintCenterYOffset = getTextCenterYOffset(mPaintHint.getFontMetrics()); mWidthOfHintText = getTextWidth(mHintText, mPaintHint); if(mPaintText == null){ throw new IllegalArgumentException( "mPaintText should not be null." ); } mPaintText.setTextSize(mTextSizeSelected); mTextSizeSelectedCenterYOffset = getTextCenterYOffset(mPaintText.getFontMetrics()); mPaintText.setTextSize(mTextSizeNormal); mTextSizeNormalCenterYOffset = getTextCenterYOffset(mPaintText.getFontMetrics()); } private void updateNotWrapYLimit(){ mNotWrapLimitYTop = 0; mNotWrapLimitYBottom = -mShowCount * mItemHeight; if(mDisplayedValues != null){ mNotWrapLimitYTop = (getOneRecycleSize() - mShowCount / 2 - 1)* mItemHeight; mNotWrapLimitYBottom = -(mShowCount / 2) * mItemHeight; } } private float downYGlobal = 0 ; private float downY = 0; private float currY = 0; private int limitY(int currDrawGlobalYPreferred){ if(mWrapSelectorWheel && mWrapSelectorWheelCheck) return currDrawGlobalYPreferred; if(currDrawGlobalYPreferred < mNotWrapLimitYBottom){ currDrawGlobalYPreferred = mNotWrapLimitYBottom; }else if(currDrawGlobalYPreferred > mNotWrapLimitYTop){ currDrawGlobalYPreferred = mNotWrapLimitYTop; } return currDrawGlobalYPreferred; } private boolean mFlagMayPress = false; @Override public boolean onTouchEvent(MotionEvent event) { if(mItemHeight == 0) return true; if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); currY = event.getY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: mFlagMayPress = true; mHandlerInNewThread.removeMessages(HANDLER_WHAT_REFRESH); stopScrolling(); downY = currY; downYGlobal = mCurrDrawGlobalY; onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: float spanY = downY - currY; if(mFlagMayPress && (-mScaledTouchSlop < spanY && spanY < mScaledTouchSlop)){ }else{ mFlagMayPress = false; mCurrDrawGlobalY = limitY((int)(downYGlobal + spanY)); calculateFirstItemParameterByGlobalY(); invalidate(); } onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); break; case MotionEvent.ACTION_UP: if(mFlagMayPress){ click(event); }else { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); int velocityY = (int) (velocityTracker.getYVelocity() * mFriction); if (Math.abs(velocityY) > mMiniVelocityFling) { mScroller.fling(0, mCurrDrawGlobalY, 0, -velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, limitY(Integer.MIN_VALUE), limitY(Integer.MAX_VALUE)); invalidate(); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0); releaseVelocityTracker(); } break; case MotionEvent.ACTION_CANCEL: downYGlobal = mCurrDrawGlobalY; stopScrolling(); mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0); break; } return true ; } private void click(MotionEvent event){ float y = event.getY(); for(int i = 0; i < mShowCount; i++){ if(mItemHeight * i <= y && y < mItemHeight * (i + 1)){ clickItem(i); break; } } } private void clickItem(int showCountIndex){ if(0 <= showCountIndex && showCountIndex < mShowCount) { //clicked the showCountIndex of the view scrollByIndexSmoothly(showCountIndex - mShowCount/2); }else{ //wrong } } private float getTextCenterYOffset(Paint.FontMetrics fontMetrics){ if(fontMetrics == null) return 0; return Math.abs(fontMetrics.top + fontMetrics.bottom)/2; } private int mViewWidth; private int mViewHeight; private int mItemHeight; private float dividerY0; private float dividerY1; private float mViewCenterX; //defaultPickedIndex relative to the shown part private void correctPositionByDefaultValue(int defaultPickedIndex, boolean wrap){ mCurrDrawFirstItemIndex = defaultPickedIndex - (mShowCount - 1) / 2; mCurrDrawFirstItemIndex = getIndexByRawIndex(mCurrDrawFirstItemIndex, getOneRecycleSize(), wrap); if(mItemHeight == 0){ mCurrentItemIndexEffect = true; }else { mCurrDrawGlobalY = mCurrDrawFirstItemIndex * mItemHeight; mInScrollingPickedOldValue = mCurrDrawFirstItemIndex + mShowCount / 2; mInScrollingPickedOldValue = mInScrollingPickedOldValue % getOneRecycleSize(); if (mInScrollingPickedOldValue < 0){ mInScrollingPickedOldValue = mInScrollingPickedOldValue + getOneRecycleSize(); } mInScrollingPickedNewValue = mInScrollingPickedOldValue; calculateFirstItemParameterByGlobalY(); } } //first shown item's content index, corresponding to the Index of mDisplayedValued private int mCurrDrawFirstItemIndex = 0; //the first shown item's Y private int mCurrDrawFirstItemY = 0; //global Y corresponding to scroller private int mCurrDrawGlobalY = 0; @Override public void computeScroll() { if(mItemHeight == 0) return; if (mScroller.computeScrollOffset()) { mCurrDrawGlobalY = mScroller.getCurrY(); calculateFirstItemParameterByGlobalY(); postInvalidate(); } } private void calculateFirstItemParameterByGlobalY(){ mCurrDrawFirstItemIndex = (int) Math.floor((float)mCurrDrawGlobalY / mItemHeight); mCurrDrawFirstItemY = -(mCurrDrawGlobalY - mCurrDrawFirstItemIndex * mItemHeight); if (mOnValueChangeListenerInScrolling != null){ if (-mCurrDrawFirstItemY > mItemHeight / 2){ mInScrollingPickedNewValue = mCurrDrawFirstItemIndex + 1 + mShowCount / 2; }else{ mInScrollingPickedNewValue = mCurrDrawFirstItemIndex + mShowCount / 2; } mInScrollingPickedNewValue = mInScrollingPickedNewValue % getOneRecycleSize(); if (mInScrollingPickedNewValue < 0){ mInScrollingPickedNewValue = mInScrollingPickedNewValue + getOneRecycleSize(); } if (mInScrollingPickedOldValue != mInScrollingPickedNewValue){ respondPickedValueChangedInScrolling(mInScrollingPickedOldValue, mInScrollingPickedNewValue); } mInScrollingPickedOldValue = mInScrollingPickedNewValue; } } private void releaseVelocityTracker() { if(mVelocityTracker != null) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } private void updateMaxWHOfDisplayedValues(boolean needRequestLayout){ updateMaxWidthOfDisplayedValues(); updateMaxHeightOfDisplayedValues(); if(needRequestLayout && (mSpecModeW == MeasureSpec.AT_MOST || mSpecModeH == MeasureSpec.AT_MOST)){ mHandlerInMainThread.sendEmptyMessage(HANDLER_WHAT_REQUEST_LAYOUT); } } private int mSpecModeW = MeasureSpec.UNSPECIFIED; private int mSpecModeH = MeasureSpec.UNSPECIFIED; private int measureWidth(int measureSpec) { int result; int specMode = mSpecModeW = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { result = specSize; } else { int marginOfHint = Math.max(mWidthOfHintText, mWidthOfAlterHint) == 0 ? 0 : mMarginEndOfHint; int gapOfHint = Math.max(mWidthOfHintText, mWidthOfAlterHint) == 0 ? 0 : mMarginStartOfHint; int maxWidth = Math.max(mMaxWidthOfAlterArrayWithMeasureHint, Math.max(mMaxWidthOfDisplayedValues, mMaxWidthOfAlterArrayWithoutMeasureHint) + 2 * (gapOfHint + Math.max(mWidthOfHintText, mWidthOfAlterHint) + marginOfHint + 2 * mItemPaddingHorizontal)); result = this.getPaddingLeft() + this.getPaddingRight() + maxWidth; //MeasureSpec.UNSPECIFIED if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result; } private int measureHeight(int measureSpec) { int result; int specMode = mSpecModeH = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { result = specSize; } else { int maxHeight = mShowCount * (mMaxHeightOfDisplayedValues + 2 * mItemPaddingVertical); result = this.getPaddingTop() + this.getPaddingBottom() + maxHeight; //MeasureSpec.UNSPECIFIED if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawContent(canvas); drawLine(canvas); drawHint(canvas); } private void drawContent(Canvas canvas){ int index; int textColor; float textSize; float fraction = 0f; // fraction of the item in state between normal and selected, in[0, 1] float textSizeCenterYOffset; for(int i = 0; i < mShowCount + 1; i++){ float y = mCurrDrawFirstItemY + mItemHeight * i; index = getIndexByRawIndex(mCurrDrawFirstItemIndex + i, getOneRecycleSize(), mWrapSelectorWheel && mWrapSelectorWheelCheck); if(i == mShowCount / 2){ //this will be picked fraction = (float)(mItemHeight + mCurrDrawFirstItemY) / mItemHeight; textColor = getEvaluateColor(fraction, mTextColorNormal, mTextColorSelected); textSize = getEvaluateSize(fraction, mTextSizeNormal, mTextSizeSelected); textSizeCenterYOffset = getEvaluateSize(fraction, mTextSizeNormalCenterYOffset, mTextSizeSelectedCenterYOffset); }else if(i == mShowCount / 2 + 1){ textColor = getEvaluateColor(1 - fraction, mTextColorNormal, mTextColorSelected); textSize = getEvaluateSize(1 - fraction, mTextSizeNormal, mTextSizeSelected); textSizeCenterYOffset = getEvaluateSize(1 - fraction, mTextSizeNormalCenterYOffset, mTextSizeSelectedCenterYOffset); }else{ textColor = mTextColorNormal; textSize = mTextSizeNormal; textSizeCenterYOffset = mTextSizeNormalCenterYOffset; } mPaintText.setColor(textColor); mPaintText.setTextSize(textSize); if(0 <= index && index < getOneRecycleSize()){ CharSequence str = mDisplayedValues[index + mMinShowIndex]; if (mTextEllipsize != null) { str = TextUtils.ellipsize(str, mPaintText, getWidth() - 2 * mItemPaddingHorizontal, getEllipsizeType()); } canvas.drawText(str.toString(), mViewCenterX, y + mItemHeight / 2 + textSizeCenterYOffset, mPaintText); } else if(!TextUtils.isEmpty(mEmptyItemHint)){ canvas.drawText(mEmptyItemHint, mViewCenterX, y + mItemHeight / 2 + textSizeCenterYOffset, mPaintText); } } } private TextUtils.TruncateAt getEllipsizeType() { switch (mTextEllipsize) { case TEXT_ELLIPSIZE_START: return TextUtils.TruncateAt.START; case TEXT_ELLIPSIZE_MIDDLE: return TextUtils.TruncateAt.MIDDLE; case TEXT_ELLIPSIZE_END: return TextUtils.TruncateAt.END; default: throw new IllegalArgumentException( "Illegal text ellipsize type." ); } } private void drawLine(Canvas canvas){ if(mShowDivider){ canvas.drawLine(getPaddingLeft() + mDividerMarginL, dividerY0, mViewWidth - getPaddingRight() - mDividerMarginR, dividerY0, mPaintDivider); canvas.drawLine(getPaddingLeft() + mDividerMarginL, dividerY1, mViewWidth - getPaddingRight() - mDividerMarginR, dividerY1, mPaintDivider); } } private void drawHint(Canvas canvas){ if(TextUtils.isEmpty(mHintText)) return; canvas.drawText(mHintText, mViewCenterX + (mMaxWidthOfDisplayedValues + mWidthOfHintText)/2 + mMarginStartOfHint, (dividerY0 + dividerY1) / 2 + mTextSizeHintCenterYOffset, mPaintHint); } private void updateMaxWidthOfDisplayedValues(){ float savedTextSize = mPaintText.getTextSize(); mPaintText.setTextSize(mTextSizeSelected); mMaxWidthOfDisplayedValues = getMaxWidthOfTextArray(mDisplayedValues, mPaintText); mMaxWidthOfAlterArrayWithMeasureHint = getMaxWidthOfTextArray(mAlterTextArrayWithMeasureHint, mPaintText); mMaxWidthOfAlterArrayWithoutMeasureHint = getMaxWidthOfTextArray(mAlterTextArrayWithoutMeasureHint, mPaintText); mPaintText.setTextSize(mTextSizeHint); mWidthOfAlterHint = getTextWidth(mAlterHint, mPaintText); mPaintText.setTextSize(savedTextSize); } private int getMaxWidthOfTextArray(CharSequence[] array, Paint paint){ if(array == null){ return 0; } int maxWidth = 0; for(CharSequence item : array){ if(item != null){ int itemWidth = getTextWidth(item, paint); maxWidth = Math.max(itemWidth, maxWidth); } } return maxWidth; } private int getTextWidth(CharSequence text, Paint paint){ if(!TextUtils.isEmpty(text)){ return (int)(paint.measureText(text.toString()) + 0.5f); } return 0; } private void updateMaxHeightOfDisplayedValues(){ float savedTextSize = mPaintText.getTextSize(); mPaintText.setTextSize(mTextSizeSelected); mMaxHeightOfDisplayedValues = (int)(mPaintText.getFontMetrics().bottom - mPaintText.getFontMetrics().top + 0.5); mPaintText.setTextSize(savedTextSize); } private void updateContentAndIndex(String[] newDisplayedValues){ mMinShowIndex = 0; mMaxShowIndex = newDisplayedValues.length - 1; mDisplayedValues = newDisplayedValues; updateWrapStateByContent(); } private void updateContent(String[] newDisplayedValues){ mDisplayedValues = newDisplayedValues; updateWrapStateByContent(); } //used in setDisplayedValues private void updateValue(){ inflateDisplayedValuesIfNull(); updateWrapStateByContent(); mMinShowIndex = 0; mMaxShowIndex = mDisplayedValues.length - 1; } private void updateValueForInit(){ inflateDisplayedValuesIfNull(); updateWrapStateByContent(); if(mMinShowIndex == -1){ mMinShowIndex = 0; } if(mMaxShowIndex == -1){ mMaxShowIndex = mDisplayedValues.length - 1; } setMinAndMaxShowIndex(mMinShowIndex, mMaxShowIndex, false); } private void inflateDisplayedValuesIfNull(){ if(mDisplayedValues == null) { mDisplayedValues = new String[1]; mDisplayedValues[0] = "0" ; } } private void updateWrapStateByContent(){ mWrapSelectorWheelCheck = mDisplayedValues.length <= mShowCount ? false : true; } private int refineValueByLimit(int value, int minValue, int maxValue, boolean wrap) { if (wrap) { if (value > maxValue) { value = (value - maxValue) % getOneRecycleSize() + minValue - 1; } else if (value < minValue) { value = (value - minValue) % getOneRecycleSize() + maxValue + 1; } return value; } else { if(value > maxValue){ value = maxValue; }else if(value < minValue){ value = minValue; } return value; } } private void stopRefreshing(){ if (mHandlerInNewThread != null){ mHandlerInNewThread.removeMessages(HANDLER_WHAT_REFRESH); } } public void stopScrolling(){ if(mScroller != null){ if(!mScroller.isFinished()){ mScroller.startScroll(0, mScroller.getCurrY(), 0, 0, 1); mScroller.abortAnimation(); postInvalidate(); } } } public void stopScrollingAndCorrectPosition(){ stopScrolling(); if (mHandlerInNewThread != null){ mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0); } } private Message getMsg(int what){ return getMsg(what, 0, 0, null); } private Message getMsg(int what, int arg1, int arg2, Object obj){ Message msg = Message.obtain(); msg.what = what; msg.arg1 = arg1; msg.arg2 = arg2; msg.obj = obj; return msg; } //===tool functions===// private boolean isStringEqual(String a, String b){ if(a == null){ if(b == null){ return true; }else{ return false; } }else{ return a.equals(b); } } private int sp2px(Context context, float spValue) { final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; return (int) (spValue * fontScale + 0.5f); } private int dp2px(Context context, float dpValue) { final float densityScale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * densityScale + 0.5f); } private int getEvaluateColor(float fraction, int startColor, int endColor){ int a, r, g, b; int sA = (startColor & 0xff000000) >>> 24; int sR = (startColor & 0x00ff0000) >>> 16; int sG = (startColor & 0x0000ff00) >>> 8; int sB = (startColor & 0x000000ff) >>> 0; int eA = (endColor & 0xff000000) >>> 24; int eR = (endColor & 0x00ff0000) >>> 16; int eG = (endColor & 0x0000ff00) >>> 8; int eB = (endColor & 0x000000ff) >>> 0; a = (int)(sA + (eA - sA) * fraction); r = (int)(sR + (eR - sR) * fraction); g = (int)(sG + (eG - sG) * fraction); b = (int)(sB + (eB - sB) * fraction); return a << 24 | r << 16 | g << 8 | b; } private float getEvaluateSize(float fraction, float startSize, float endSize){ return startSize + (endSize - startSize) * fraction; } private String[] convertCharSequenceArrayToStringArray(CharSequence[] charSequences){ if(charSequences == null) return null; String[] ret = new String[charSequences.length]; for(int i = 0; i < charSequences.length; i++){ ret[i] = charSequences[i].toString(); } return ret; } }
一个简单的调用类:
package com.bestgo.callshow; import android.os.Bundle; import android.app.Activity; import android.support.v7.app.AppCompatActivity; import android.view.View; import com.bestgo.callshow.custom_control.NumberPickerView; public class ActivitySettingPickerviewActivity extends AppCompatActivity implements View.OnClickListener,NumberPickerView.OnScrollListener,NumberPickerView.OnValueChangeListener, NumberPickerView.OnValueChangeListenerInScrolling{ private NumberPickerView picker; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_setting_pickerview); picker = (NumberPickerView) findViewById(R.id.picker); picker.setOnScrollListener(this); picker.setOnValueChangedListener((NumberPickerView.OnValueChangeListener) this); picker.setOnValueChangeListenerInScrolling((NumberPickerView.OnValueChangeListenerInScrolling) this); String[] display_2 = getResources().getStringArray(R.array.test_display_2); picker.refreshByNewDisplayedValues(display_2); } @Override public void onValueChange(NumberPickerView picker, int oldVal, int newVal) { } @Override public void onValueChangeInScrolling(NumberPickerView picker, int oldVal, int newVal) { } @Override public void onScrollStateChange(NumberPickerView view, int scrollState) { } @Override public void onClick(View v) { } }
是不是需要layout界面??
<com.bestgo.callshow.custom_control.NumberPickerView android :id= "@+id/picker" android :layout_width= "match_parent" android :layout_height= "240dp" android :layout_centerHorizontal= "true" android :layout_gravity= "center" android :background= "@color/base_white" android :contentDescription= "test_number_picker_view" app :npv_ItemPaddingHorizontal= "5dp" app :npv_ItemPaddingVertical= "5dp" app :npv_ShowCount= "5" app :npv_TextSizeNormal= "14sp" app :npv_TextSizeSelected= "20sp" app :npv_WrapSelectorWheel= "false" />