1. 程式人生 > >Android 開發 pickerview 自定義選擇器

Android 開發 pickerview 自定義選擇器

超好用的類:

在專案直接寫入,可以自定義選擇器, 

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 : [email protected] * 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; }