标签:
DatePicker在android其实是有提供的一个控件,相信有不少的人使用过它,但是这个控件的外观我们只能做一些简单的设定(原生的),如果我们有更高需求,希望能自定义我们的datepicker的外观,希望赋予它更多的功能,我们就需要自定义一个datepciker控件。
在github上,我发现了一个chenglei1986/DatePicker的项目,可以实现上面的需求。地址是https://github.com/chenglei1986/DatePicker
这个自定义控件非常灵活,通过学习这个控件的源码,我们可以进一步了解自定义控件的方法,特别是scroller等滑动功能的使用,这也是我为什么选择这个控件来解析的原因。
如果对scroller等功能不清楚,可以看本专栏之前的文章。http://blog.csdn.net/crazy__chen/article/details/45896961
这个datepicker控件代码比较复杂,本质上是由三个自定义的numberpicker组成的。下面我就先说明numberpicker的写法,然后datepicker就会变得简单。
先看看效果图:
针对于numberpicker,我们先看看我们需要做些什么。
首先是控件绘制,可以看到每个numberpicker,有三个选项组成,每个选项之间,有一个条边,而除了选中项是完全不透明的以外,其他没有选中项是有一定的透明度的,而且这个透明度是从外到内主键减少的。
ok,如果我们希望绘制一个这样的视图,应该说并不困难,无非就是drawline,drawtext之类的。
另外我们看到11的右上角还有一个a,这是这个开源控件自定义的功能,也就是我们可以给选中项增加一个右上角标记(按实际需求,可以是“年”,“月”,“日"等)
绘制并不困难,难的是滑动。
滑动有两种,一种是拖动,也就是说手指没有离开屏幕。
对于拖动,我们可以在action_move里面,更新每个选项的坐标,假设我有一个数组{8,9,10,11,12,13}作为选项,每个选项都带有一个绘制的x,y起始坐标,那么我们就可以逐个绘制它们了。而move的时候,我们可以根据偏移量修改每个选项的x,y坐标,重新绘制,从而产生拖动的效果。
这个想法是可行的,但是问题在于,如果我们有一百个选项,是不是每个都要按照顺序画出来,那岂不是很浪费。另外,怎么保证选中项在正中间呢?怎么使滑动到尾部,又循环回头部呢,这是几个我们需要思考的问题,在接下来的源码中,我们会有解决方法。
另外一种是手指离开以后的自由滑动
这样的滑动,我们需要使用scroller来实现,另外因为滑动到的目标,是根据滑动的初始速度来决定的,我们很自然想的,要使用scroller的fling()方法
接下面是源码,我们先来看一下属性值,属性值很多,我大部分都写了注释,大家可以在后面的源码过程中,忘记了某个属性的含义,可以对照着看一下
public class NumberPicker extends View { //基本设置 /** * picker宽度 */ private int mWidth; /** * picker高度 */ private int mHeight; /** * 声效 */ private Sound mSound; /** * 是否开启声效 */ private boolean mSoundEffectEnable = true; /** * 用于修改最大滑动速度(比例) */ private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; /** * 最小滑动速度 */ private int mMinimumFlingVelocity; /** * 最大滑动速度 */ private int mMaximumFlingVelocity; /** * 背景颜色 */ private int mBackgroundColor; /** * 默认背景颜色 */ private static final int DEFAULT_BACKGROUND_COLOR = Color.rgb(255, 255, 255); //数值设置 /** * 起始值 */ private int mStartNumber; /** * 终值 */ private int mEndNumber; /** * 当前值 */ private int mCurrentNumber; /** * 数值数组,存取所有选项的值 */ private int[] mNumberArray; /** * 当前值index */ private int mCurrNumIndex; //边条设置 /** * 条边画笔 */ private TextPaint mTextPaintHighLight; /** * 默认条边颜色 */ private static final int DEFAULT_TEXT_COLOR_HIGH_LIGHT = Color.rgb(0, 150, 71); /** * 条边颜色 */ private int mTextColorHighLight; /** * 条边大小 */ private float mTextSizeHighLight; /** * 默认条边大小 */ private static final float DEFAULT_TEXT_SIZE_HIGH_LIGHT = 36; /** * 边条矩阵 */ private Rect mTextBoundsHighLight; /** * 边条画笔 */ private Paint mLinePaint; /** * 设置边条粗度 */ private static final int lineWidth = 4; //选项设置 /** * 选项画笔 */ private TextPaint mTextPaintNormal; /** * 选项字体颜色 */ private int mTextColorNormal; /** * 默认选项字体颜色 */ private static final int DEFAULT_TEXT_COLOR_NORMAL = Color.rgb(0, 0, 0); /** * 选项字体大小 */ private float mTextSizeNormal; /** * 默认选项字体大小 */ private static final float DEFAULT_TEXT_SIZE_NORMAL = 32; /** * 两个选项之间的垂直距离 */ private int mVerticalSpacing; /** * 默认两个选项之间的垂直距离 */ private static final int DEFAULT_VERTICAL_SPACING = 16; /** * 选项文字矩阵 */ private Rect mTextBoundsNormal; /** * 每个picker每次显示多少选项=边条数目+1 */ private int mLines; /** * 默认选项数目=默认边条数目+1 */ private static final int DEFAULT_LINES = 3; //遮罩设置 /** * 上遮罩画笔 */ private Paint mShaderPaintTop; /** * 下遮罩画笔 */ private Paint mShaderPaintBottom; //右上角文字设置 /** * 高亮数字的右上角显示的文字 */ private String mFlagText; /** * 右上角文字颜色 */ private int mFlagTextColor; /** * 右上角文字大小 */ private float mFlagTextSize; /** * 默认右上角文字大小 */ private static final float DEFAULT_FLAG_TEXT_SIZE = 12; /** * 默认右上角文字颜色 */ private static final int DEFAULT_FLAG_TEXT_COLOR = Color.rgb(148, 148, 148); /** *右上角文字画笔 */ private TextPaint mTextPaintFlag; /** * 存储当前显示项 * 长度为每次显示的选项数目+4 */ private NumberHolder[] mTextYAxisArray; /** * 起始绘制Y坐标=控件高度/2-3*选项高度 */ private int mStartYPos; /** * 起始结束Y坐标=控件高度/2+3*选项高度 */ private int mEndYPos; /** * 自定义选项数组 * 除了数字以外,我们还可以传入字符串数组,从而显示字符串选项 */ private String[] mTextArray; /** * getScaledTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。如果小于这个距离就不触发移动控件 */ private int mTouchSlop; /** * 表示整个picker */ private RectF mHighLightRect; private Rect mTextBoundsFlag; private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; private int mTouchAction = MotionEvent.ACTION_CANCEL; /** * 该scroller用于滚动 */ private Scroller mFlingScroller; /** * 该scroller用于保证选项位置正确 */ private Scroller mAdjustScroller; private int mStartY; private int mCurrY; private int mOffectY; private int mSpacing; private boolean mCanScroll; /** * 用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率 */ private VelocityTracker mVelocityTracker; private OnScrollListener mOnScrollListener; private OnValueChangeListener mOnValueChangeListener; private float mDensity;
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="NumberPicker"> <attr name="textColorHighLight" format="color" /> <attr name="textColorNormal" format="color" /> <attr name="textSizeHighLight" format="float|dimension" /> <attr name="textSizeNormal" format="float|dimension" /> <attr name="startNumber" format="integer" /> <attr name="endNumber" format="integer" /> <attr name="currentNumber" format="integer" /> <attr name="verticalSpacing" format="dimension" /> <attr name="flagText" format="string|reference" /> <attr name="flagTextSize" format="dimension" /> <attr name="flagTextColor" format="color" /> <attr name="backgroundColor" format="color" /> <attr name="lines" format="integer" /> </declare-styleable> </resources>例如:
<com.example.androidtest.NumberPicker android:id="@+id/day_picker" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:padding="16dp" app:flagText="asdasd" app:flagTextSize="30dp" app:flagTextColor="#abcdef" app:startNumber="1" app:endNumber="31" app:currentNumber="11" app:textColorNormal="#000000" app:textSizeHighLight="24dp" app:textColorHighLight="#abcdef" app:textSizeNormal="22dp" app:verticalSpacing="50dp" app:lines="3" />然后在代码里面读取属性值就可以了,我们来看构造函数
public NumberPicker(Context context) { this(context, null); } public NumberPicker(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDensity = getResources().getDisplayMetrics().density; readAttrs(context, attrs, defStyleAttr); init(); } /** * 读取自定义属性值 * @param context * @param attrs * @param defStyleAttr */ private void readAttrs(Context context, AttributeSet attrs, int defStyleAttr) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberPicker, defStyleAttr, 0); mTextColorHighLight = a.getColor(R.styleable.NumberPicker_textColorHighLight, DEFAULT_TEXT_COLOR_HIGH_LIGHT); mTextColorNormal = a.getColor(R.styleable.NumberPicker_textColorNormal, DEFAULT_TEXT_COLOR_NORMAL); mTextSizeHighLight = a.getDimension(R.styleable.NumberPicker_textSizeHighLight, DEFAULT_TEXT_SIZE_HIGH_LIGHT * mDensity); mTextSizeNormal = a.getDimension(R.styleable.NumberPicker_textSizeNormal, DEFAULT_TEXT_SIZE_NORMAL * mDensity); mStartNumber = a.getInteger(R.styleable.NumberPicker_startNumber, 0); mEndNumber = a.getInteger(R.styleable.NumberPicker_endNumber, 0); mCurrentNumber = a.getInteger(R.styleable.NumberPicker_currentNumber, 0); mVerticalSpacing = (int) a.getDimension(R.styleable.NumberPicker_verticalSpacing, DEFAULT_VERTICAL_SPACING * mDensity); mFlagText = a.getString(R.styleable.NumberPicker_flagText); mFlagTextColor = a.getColor(R.styleable.NumberPicker_flagTextColor, DEFAULT_FLAG_TEXT_COLOR); mFlagTextSize = a.getDimension(R.styleable.NumberPicker_flagTextSize, DEFAULT_FLAG_TEXT_SIZE * mDensity); mBackgroundColor = a.getColor(R.styleable.NumberPicker_backgroundColor, DEFAULT_BACKGROUND_COLOR); mLines = a.getInteger(R.styleable.NumberPicker_lines, DEFAULT_LINES); }readAttrs()函数读取了属性值,包括背景颜色,选项数目,选项的范围(例如月份是1-12),当前选项,文字的大小,颜色,条边的颜色等
接下来是一个初始化函数
/** * 初始化 */ private void init() { verifyNumber(); initPaint(); initRects(); measureText(); //Configuration包含的方法和常量是可用于UI 超时,大小和距离的设置 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; mFlingScroller = new Scroller(getContext(), null); //DecelerateInterpolator表示在动画开始的地方快然后慢 mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); }这个函数做了几个初始化工作,包括画笔,测量矩形,选项的检查等。
另外ViewConfiguration类包含了UI设定的常见内容,我们这里获得了滑动的最小距离(要超过这个距离,才算滑动了屏幕),滑动的最大和最小速度等,这些值会在实现滑动效果的过程中使用到
另外就是实例化两个Scroller,有人会问,为什么是两个,一个不够吗?
不够,mFlingScroller是用于我们松开手指以后,保证选项继续滑动效果的,但是我们有一个很重要的问题,就是要保证选项的位置,例如选中项,它就要在整个numberpicker的正中间。我们知道fling函数导致的目的坐标是不确定的(根据滑动手势速度计算出来)
为了确保上面的要求,我们又有一个mAdjustScroller使选项回到正确的位置上。大家下载例子文件以后,可以测试一下,当选中项超过中间位置而整个滑动快停止时,选中项又会往回滑动,从而回到正确的位置上。要实现两个方向的滑动,我们当然需要两个Scroller。
下面分别看几个初始化函数
/** * 检查当前起始值,终值是否合理 * 生成数值数组 */ private void verifyNumber() { if (mStartNumber < 0 || mEndNumber < 0) {//小于0,抛出异常 throw new IllegalArgumentException("number can not be negative"); } if (mStartNumber > mEndNumber) { mEndNumber = mStartNumber; } if (mCurrentNumber < mStartNumber) { mCurrentNumber = mStartNumber; } if (mCurrentNumber > mEndNumber) { mCurrentNumber = mEndNumber; } mNumberArray = new int[mEndNumber - mStartNumber + 1]; for (int i = 0; i < mNumberArray.length; i++) {//生成数值数组 mNumberArray[i] = mStartNumber + i; } mCurrNumIndex = mCurrentNumber - mStartNumber;//获取当前值的index mTextYAxisArray = new NumberHolder[mLines + 4]; } /** * 初始化各种画笔 */ private void initPaint() { mTextPaintHighLight = new TextPaint(); mTextPaintHighLight.setTextSize(mTextSizeHighLight); mTextPaintHighLight.setColor(mTextColorHighLight); mTextPaintHighLight.setFlags(TextPaint.ANTI_ALIAS_FLAG); mTextPaintHighLight.setTextAlign(Align.CENTER); mTextPaintNormal = new TextPaint(); mTextPaintNormal.setTextSize(mTextSizeNormal); mTextPaintNormal.setColor(mTextColorNormal); mTextPaintNormal.setFlags(TextPaint.ANTI_ALIAS_FLAG); mTextPaintNormal.setTextAlign(Align.CENTER); mTextPaintFlag = new TextPaint(); mTextPaintFlag.setTextSize(mFlagTextSize); mTextPaintFlag.setColor(mFlagTextColor); mTextPaintFlag.setFlags(TextPaint.ANTI_ALIAS_FLAG); mTextPaintFlag.setTextAlign(Align.LEFT); mLinePaint = new Paint(); mLinePaint.setColor(mTextColorHighLight); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setStrokeWidth(lineWidth * mDensity); mShaderPaintTop = new Paint(); mShaderPaintBottom = new Paint(); } /** * 初始化矩形 */ private void initRects() { mTextBoundsHighLight = new Rect(); mTextBoundsNormal = new Rect(); mTextBoundsFlag = new Rect(); }上面的函数没有什么可以说的,无法就是根据属性值,做一些设定
比较重要的是这一句
mTextYAxisArray = new NumberHolder[mLines + 4];
前面说过,我们不可能把每个选项都绘制出来,所以在这里我们每次只绘制mLines+4个,接下面在滑动的时候,我们只有维护这个mTextYAxisArray数组就可以了
接下来还有一个初始化函数
/** * 测量文字边界 */ private void measureText() { /* * 保证不同长度的数值边界相同 * 例如"2014" 到 "0000". */ String text = String.valueOf(mEndNumber); int length = text.length(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < length; i++) { builder.append("0"); } text = builder.toString(); //会按严格按照Paint的样式,绘制出文字的边界,调用native层去测量 mTextPaintHighLight.getTextBounds(text, 0, text.length(), mTextBoundsHighLight); mTextPaintNormal.getTextBounds(text, 0, text.length(), mTextBoundsNormal); if (mFlagText != null) { mTextPaintFlag.getTextBounds(mFlagText, 0, mFlagText.length(), mTextBoundsFlag); } }
这个函数计算出最长的文字长度(稍后在画图时会用到),另外还是讲每个选项中,整个字符串的长宽,保留在了Rect中(其实前面定义的Rect就是用来保存长宽等属性的,当然我们也可以设置一下属性值来代替rect,但是使用Rect来记录,无疑更方便)
初始化完毕,然后是控件的绘制,对于绘制,我们先来看onMeasure()方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { // 父控件已经告诉picker要多宽 mWidth = widthSize; } else {//否则 //宽度=边条宽度+左内边距+右内边距+右上角文字宽度+6 mWidth = mTextBoundsHighLight.width() + getPaddingLeft() + getPaddingRight() + mTextBoundsFlag.width() + 6; } if (heightMode == MeasureSpec.EXACTLY) { // 父控件已经告诉picker要多高 mHeight = heightSize; } else {//否则 //高度=选项数目*高度+边条数目*选项垂直距离+上内边距+下内边距 mHeight = mLines * mTextBoundsNormal.height() + (mLines - 1) * mVerticalSpacing + getPaddingTop() + getPaddingBottom(); } setMeasuredDimension(mWidth, mHeight); /* * Do some initialization work when the view got a size */ if (null == mHighLightRect) { //RectF的坐标是用单精度浮点型表示的 mHighLightRect = new RectF();//表示整个picker mHighLightRect.left = 0; mHighLightRect.right = mWidth; mHighLightRect.top = (mHeight - mTextBoundsHighLight.height() - mVerticalSpacing) / 2; mHighLightRect.bottom = (mHeight + mTextBoundsHighLight.height() + mVerticalSpacing) / 2; //上遮罩 Shader topShader = new LinearGradient(0, 0, 0, mHighLightRect.top, new int[] { mBackgroundColor & 0xDFFFFFFF, mBackgroundColor & 0xCFFFFFFF, mBackgroundColor & 0x00FFFFFF }, null, Shader.TileMode.CLAMP); //下遮罩 Shader bottomShader = new LinearGradient(0, mHighLightRect.bottom, 0, mHeight, new int[] { mBackgroundColor & 0x00FFFFFF, mBackgroundColor & 0xCFFFFFFF, mBackgroundColor & 0xDFFFFFFF }, null, Shader.TileMode.CLAMP); mShaderPaintTop.setShader(topShader); mShaderPaintBottom.setShader(bottomShader); //选项高度=垂直距离+文字高度 mSpacing = mVerticalSpacing + mTextBoundsNormal.height(); //起始绘制Y坐标=控件高度/2-3*选项高度 mStartYPos = mHeight / 2 - 3 * mSpacing; //起始结束Y坐标=控件高度/2+3*选项高度 mEndYPos = mHeight / 2 + 3 * mSpacing; initTextYAxisArray(); } }
另外还有上下遮罩的shadow,还有起始绘制的X,Y坐标,还有每个绘制项的高度mSpacing
绘制的X,Y坐标,我们可以从这个其实坐标开始绘制第一个选项,然后Y+mSpacing绘制第二个选项,以此类推
再来看initTextYAxisArray()方法
/** * 初始化选项文字的Y坐标 */ private void initTextYAxisArray() { for (int i = 0; i < mTextYAxisArray.length; i++) { //保证选中项在正中间 NumberHolder textYAxis = new NumberHolder(mCurrNumIndex - 3 + i, mStartYPos + i * mSpacing); if (textYAxis.mmIndex > mNumberArray.length - 1) {//如果选中项之后不够3项,用头部补足 textYAxis.mmIndex -= mNumberArray.length; } else if (textYAxis.mmIndex < 0) {//如果选中项之前不够3项,用尾部补足 textYAxis.mmIndex += mNumberArray.length; } mTextYAxisArray[i] = textYAxis; } }前面已经说到,每个选择最好知道自己开始绘制的x,y坐标,然后绘制就可以了,那么我们很自然会使用面向对象编程,每个选项都是这样一个对象,除了坐标,还有它所代表的,在选项数组中的序号
/** * 数字对象,保存有该数字所在的起始Y坐标,在当前显示项的index */ class NumberHolder { /** * Array index of the number in {@link #mTextYAxisArray} */ public int mmIndex; /** * 该数字的起始Y坐标 */ public int mmPos; public NumberHolder(int index, int position) { mmIndex = index; mmPos = position; } }对于initTextYAxisArray()函数的具体内容,大家可以看注释,函数的关键是保证选中项在中间。例如例子中,我们的mTextYAxisArray长度为7(3+4),那么选中项必须在这个数组的中间,如果选中项前面没有数,就应该拿尾部的数字来补足(例如选中为2,2前面有0,1,但是长度为7,说明2前面应该有三个数,那么我就需要拿末尾的数来补到0,1前面)
获得高度等绘制的必要信息以后,我们开始绘制
@Override protected void onDraw(Canvas canvas) { //绘制背景 canvas.drawColor(mBackgroundColor); //绘制两条选中数上下的边条 canvas.drawLine(0, mHighLightRect.top, mWidth, mHighLightRect.top, mLinePaint); canvas.drawLine(0, mHighLightRect.bottom, mWidth, mHighLightRect.bottom, mLinePaint); //绘制右上角文字 if (mFlagText != null) { int x = (mWidth + mTextBoundsHighLight.width() + 6) / 2; canvas.drawText(mFlagText, x, mHeight / 2, mTextPaintFlag); } //绘制选项 for (int i = 0; i < mTextYAxisArray.length; i++) { if (mTextYAxisArray[i].mmIndex >= 0 && mTextYAxisArray[i].mmIndex <= mEndNumber - mStartNumber) { String text = null; if (mTextArray != null) {//是否自定义字符数组 text = mTextArray[mTextYAxisArray[i].mmIndex]; } else { text = String.valueOf(mNumberArray[mTextYAxisArray[i].mmIndex]); } canvas.drawText( text, mWidth / 2, mTextYAxisArray[i].mmPos + mTextBoundsNormal.height() / 2, mTextPaintNormal); } } // 绘制遮罩 canvas.drawRect(0, 0, mWidth, mHighLightRect.top, mShaderPaintTop); canvas.drawRect(0, mHighLightRect.bottom, mWidth, mHeight, mShaderPaintBottom); // Scroll the number to the position where exactly they should be. // Only do this when the finger no longer touch the screen and the fling action is finished. if (MotionEvent.ACTION_UP == mTouchAction && mFlingScroller.isFinished()) { adjustYPosition(); } }绘制背景,条边,右上角文字,遮罩等,都没有什么好说的,因为它们的位置很容易确定
对于选项,我们只绘制mTextYAxisArray的内容,逐个绘制(因为选中项已经在中间了)
注意到我们有个adjustYPosition()函数,这个函数是为了让选项回到正确的位置上,所以使用到了mAdjustScroller.startScroll()
/** * 使数字滑动正确的位置(也就是说选中项要在正中间) */ private void adjustYPosition() { if (mAdjustScroller.isFinished()) { mStartY = 0; int offsetIndex = Math.round((float)(mTextYAxisArray[0].mmPos - mStartYPos) / (float)mSpacing); int position = mStartYPos + offsetIndex * mSpacing; int dy = position - mTextYAxisArray[0].mmPos; if (dy != 0) { mAdjustScroller.startScroll(0, 0, 0, dy, 300); } } }
绘制还是没有太多的困难(尽管我们还不知道怎么在滚动时去维护mTextYAxisArray)
接下来我们处理滑动的问题,一切问题的开始,在于我们手指触摸屏幕的那一刻,大家来看onTouchEvent()方法
@Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) {//是否enabled return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); //表示用于多点 触控检测点 int action = event.getActionMasked(); mTouchAction = action; if (MotionEvent.ACTION_DOWN == action) { mStartY = (int) event.getY(); if (!mFlingScroller.isFinished() || !mAdjustScroller.isFinished()) {//没有停止,强制停止 mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } else if (MotionEvent.ACTION_MOVE == action) { mCurrY = (int) event.getY(); mOffectY = mCurrY - mStartY; if (!mCanScroll && Math.abs(mOffectY) < mTouchSlop) { return false; } else { mCanScroll = true; if (mOffectY > mTouchSlop) { mOffectY -= mTouchSlop; } else if (mOffectY < -mTouchSlop) { mOffectY += mTouchSlop; } } mStartY = mCurrY; computeYPos(mOffectY); onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); invalidate(); } else if (MotionEvent.ACTION_UP == action) { mCanScroll = false; VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {//如果快速滑动 fling(initialVelocity); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else {//如果只是轻微移动 adjustYPosition(); invalidate(); } mVelocityTracker.recycle(); mVelocityTracker = null; } return true; }
down事件里面,我们取消正在进行的滑动
move事件里面,我们计算出手指移动的offset,然后调用了computeYPos()方法,这个方法用于更新mTextYAxisArray中的坐标
/** * Make {@link #mTextYAxisArray} to be a circular array * 使mTextYAxisArray变成一个循环数组 * * @param offectY */ private void computeYPos(int offectY) { for (int i = 0; i < mTextYAxisArray.length; i++) { mTextYAxisArray[i].mmPos += offectY; if (mTextYAxisArray[i].mmPos >= mEndYPos + mSpacing) {//如果新位置超出高度 mTextYAxisArray[i].mmPos -= (mLines + 2) * mSpacing;//定位到开头 mTextYAxisArray[i].mmIndex -= (mLines + 2);//更新序号 if (mTextYAxisArray[i].mmIndex < 0) { mTextYAxisArray[i].mmIndex += mNumberArray.length; } } else if (mTextYAxisArray[i].mmPos <= mStartYPos - mSpacing) {//如果新位置超出起始点 mTextYAxisArray[i].mmPos += (mLines + 2) * mSpacing;//定位到结尾 mTextYAxisArray[i].mmIndex += (mLines + 2);//更新序号 if (mTextYAxisArray[i].mmIndex > mNumberArray.length - 1) { mTextYAxisArray[i].mmIndex -= mNumberArray.length; } } if (Math.abs(mTextYAxisArray[i].mmPos - mHeight / 2) < mSpacing / 4) {//离中间距离小于mSpacing / 4 mCurrNumIndex = mTextYAxisArray[i].mmIndex;//取得当前值 int oldNumber = mCurrentNumber; mCurrentNumber = mNumberArray[mCurrNumIndex]; if (oldNumber != mCurrentNumber) { if (mOnValueChangeListener != null) { mOnValueChangeListener.onValueChange(this, oldNumber, mCurrentNumber); } // Play a sound effect if (mSound != null && mSoundEffectEnable) { mSound.playSoundEffect(); } } } } }具体做法如上,对于每个选项,我们计算出它的新位置以后,如果已经超出了界限,就需要考虑是否变更它所代表的序号
另外还有mSound,用于提供滑动时的声音
最后,当手指离开屏幕,也就是up事件当中,我们调用了fling()方法
/** * 设置快速滑动fling * @param startYVelocity */ private void fling(int startYVelocity) { int maxY = 0; if (startYVelocity > 0) {//向下滑动 maxY = (int) (mTextSizeNormal * 10); mStartY = 0; mFlingScroller.fling(0, 0, 0, startYVelocity, 0, 0, 0, maxY); } else if (startYVelocity < 0) {//向上滑动 maxY = (int) (mTextSizeNormal * 10); mStartY = maxY; mFlingScroller.fling(0, maxY, 0, startYVelocity, 0, 0, 0, maxY); } invalidate(); }然后进行位置调整,调用adjustYPosition()方法,还有清空mVelocityTracker,否则会报错
mVelocityTracker.recycle(); mVelocityTracker = null;当然,我们知道mFlingScroller的fling()其实并没有进行实际的滑动,它只是给我提供坐标
真正的滑动,在computeScroll()方法里面,通过利用fling()计算出的坐标,调用computeYPos()方法来进行
/** * 这个函数会在控件滚动的时候调用,准确来说,是在父控件调用drawchild()以后,在控件调用draw()以前 */ @Override public void computeScroll() { Scroller scroller = mFlingScroller; if (scroller.isFinished()) {//如果滚动已经停止了 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); scroller = mAdjustScroller;//使scroller为mAdjustScroller if (scroller.isFinished()) { return; } } scroller.computeScrollOffset(); mCurrY = scroller.getCurrY(); mOffectY = mCurrY - mStartY; computeYPos(mOffectY); invalidate(); mStartY = mCurrY; }
最后,贴出源码文件下载地址http://download.csdn.net/detail/kangaroo835127729/8731833和numberpicker的完整代码
package com.example.androidtest; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.support.v4.view.ViewConfigurationCompat; import android.text.TextPaint; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.DecelerateInterpolator; import android.widget.Scroller; public class NumberPicker extends View { //基本设置 /** * picker宽度 */ private int mWidth; /** * picker高度 */ private int mHeight; /** * 声效 */ private Sound mSound; /** * 是否开启声效 */ private boolean mSoundEffectEnable = true; /** * 用于修改最大滑动速度(比例) */ private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; /** * 最小滑动速度 */ private int mMinimumFlingVelocity; /** * 最大滑动速度 */ private int mMaximumFlingVelocity; /** * 背景颜色 */ private int mBackgroundColor; /** * 默认背景颜色 */ private static final int DEFAULT_BACKGROUND_COLOR = Color.rgb(255, 255, 255); //数值设置 /** * 起始值 */ private int mStartNumber; /** * 终值 */ private int mEndNumber; /** * 当前值 */ private int mCurrentNumber; /** * 数值数组,存取所有选项的值 */ private int[] mNumberArray; /** * 当前值index */ private int mCurrNumIndex; //边条设置 /** * 条边画笔 */ private TextPaint mTextPaintHighLight; /** * 默认条边颜色 */ private static final int DEFAULT_TEXT_COLOR_HIGH_LIGHT = Color.rgb(0, 150, 71); /** * 条边颜色 */ private int mTextColorHighLight; /** * 条边大小 */ private float mTextSizeHighLight; /** * 默认条边大小 */ private static final float DEFAULT_TEXT_SIZE_HIGH_LIGHT = 36; /** * 边条矩阵 */ private Rect mTextBoundsHighLight; /** * 边条画笔 */ private Paint mLinePaint; /** * 设置边条粗度 */ private static final int lineWidth = 4; //选项设置 /** * 选项画笔 */ private TextPaint mTextPaintNormal; /** * 选项字体颜色 */ private int mTextColorNormal; /** * 默认选项字体颜色 */ private static final int DEFAULT_TEXT_COLOR_NORMAL = Color.rgb(0, 0, 0); /** * 选项字体大小 */ private float mTextSizeNormal; /** * 默认选项字体大小 */ private static final float DEFAULT_TEXT_SIZE_NORMAL = 32; /** * 两个选项之间的垂直距离 */ private int mVerticalSpacing; /** * 默认两个选项之间的垂直距离 */ private static final int DEFAULT_VERTICAL_SPACING = 16; /** * 选项文字矩阵 */ private Rect mTextBoundsNormal; /** * 每个picker每次显示多少选项=边条数目+1 */ private int mLines; /** * 默认选项数目=默认边条数目+1 */ private static final int DEFAULT_LINES = 3; //遮罩设置 /** * 上遮罩画笔 */ private Paint mShaderPaintTop; /** * 下遮罩画笔 */ private Paint mShaderPaintBottom; //右上角文字设置 /** * 高亮数字的右上角显示的文字 */ private String mFlagText; /** * 右上角文字颜色 */ private int mFlagTextColor; /** * 右上角文字大小 */ private float mFlagTextSize; /** * 默认右上角文字大小 */ private static final float DEFAULT_FLAG_TEXT_SIZE = 12; /** * 默认右上角文字颜色 */ private static final int DEFAULT_FLAG_TEXT_COLOR = Color.rgb(148, 148, 148); /** *右上角文字画笔 */ private TextPaint mTextPaintFlag; /** * 存储当前显示项 * 长度为每次显示的选项数目+4 */ private NumberHolder[] mTextYAxisArray; /** * 起始绘制Y坐标=控件高度/2-3*选项高度 */ private int mStartYPos; /** * 起始结束Y坐标=控件高度/2+3*选项高度 */ private int mEndYPos; /** * 自定义选项数组 * 除了数字以外,我们还可以传入字符串数组,从而显示字符串选项 */ private String[] mTextArray; /** * getScaledTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。如果小于这个距离就不触发移动控件 */ private int mTouchSlop; /** * 表示整个picker */ private RectF mHighLightRect; private Rect mTextBoundsFlag; private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; private int mTouchAction = MotionEvent.ACTION_CANCEL; /** * 该scroller用于滚动 */ private Scroller mFlingScroller; /** * 该scroller用于保证选项位置正确 */ private Scroller mAdjustScroller; private int mStartY; private int mCurrY; private int mOffectY; private int mSpacing; private boolean mCanScroll; /** * 用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率 */ private VelocityTracker mVelocityTracker; private OnScrollListener mOnScrollListener; private OnValueChangeListener mOnValueChangeListener; private float mDensity; public NumberPicker(Context context) { this(context, null); } public NumberPicker(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDensity = getResources().getDisplayMetrics().density; readAttrs(context, attrs, defStyleAttr); init(); } /** * 读取自定义属性值 * @param context * @param attrs * @param defStyleAttr */ private void readAttrs(Context context, AttributeSet attrs, int defStyleAttr) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberPicker, defStyleAttr, 0); mTextColorHighLight = a.getColor(R.styleable.NumberPicker_textColorHighLight, DEFAULT_TEXT_COLOR_HIGH_LIGHT); mTextColorNormal = a.getColor(R.styleable.NumberPicker_textColorNormal, DEFAULT_TEXT_COLOR_NORMAL); mTextSizeHighLight = a.getDimension(R.styleable.NumberPicker_textSizeHighLight, DEFAULT_TEXT_SIZE_HIGH_LIGHT * mDensity); mTextSizeNormal = a.getDimension(R.styleable.NumberPicker_textSizeNormal, DEFAULT_TEXT_SIZE_NORMAL * mDensity); mStartNumber = a.getInteger(R.styleable.NumberPicker_startNumber, 0); mEndNumber = a.getInteger(R.styleable.NumberPicker_endNumber, 0); mCurrentNumber = a.getInteger(R.styleable.NumberPicker_currentNumber, 0); mVerticalSpacing = (int) a.getDimension(R.styleable.NumberPicker_verticalSpacing, DEFAULT_VERTICAL_SPACING * mDensity); mFlagText = a.getString(R.styleable.NumberPicker_flagText); mFlagTextColor = a.getColor(R.styleable.NumberPicker_flagTextColor, DEFAULT_FLAG_TEXT_COLOR); mFlagTextSize = a.getDimension(R.styleable.NumberPicker_flagTextSize, DEFAULT_FLAG_TEXT_SIZE * mDensity); mBackgroundColor = a.getColor(R.styleable.NumberPicker_backgroundColor, DEFAULT_BACKGROUND_COLOR); mLines = a.getInteger(R.styleable.NumberPicker_lines, DEFAULT_LINES); } /** * 初始化 */ private void init() { verifyNumber(); initPaint(); initRects(); measureText(); //Configuration包含的方法和常量是可用于UI 超时,大小和距离的设置 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; mFlingScroller = new Scroller(getContext(), null); //DecelerateInterpolator表示在动画开始的地方快然后慢 mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); } /** * 检查当前起始值,终值是否合理 * 生成数值数组 */ private void verifyNumber() { if (mStartNumber < 0 || mEndNumber < 0) {//小于0,抛出异常 throw new IllegalArgumentException("number can not be negative"); } if (mStartNumber > mEndNumber) { mEndNumber = mStartNumber; } if (mCurrentNumber < mStartNumber) { mCurrentNumber = mStartNumber; } if (mCurrentNumber > mEndNumber) { mCurrentNumber = mEndNumber; } mNumberArray = new int[mEndNumber - mStartNumber + 1]; for (int i = 0; i < mNumberArray.length; i++) {//生成数值数组 mNumberArray[i] = mStartNumber + i; } mCurrNumIndex = mCurrentNumber - mStartNumber;//获取当前值的index mTextYAxisArray = new NumberHolder[mLines + 4]; } /** * 初始化各种画笔 */ private void initPaint() { mTextPaintHighLight = new TextPaint(); mTextPaintHighLight.setTextSize(mTextSizeHighLight); mTextPaintHighLight.setColor(mTextColorHighLight); mTextPaintHighLight.setFlags(TextPaint.ANTI_ALIAS_FLAG); mTextPaintHighLight.setTextAlign(Align.CENTER); mTextPaintNormal = new TextPaint(); mTextPaintNormal.setTextSize(mTextSizeNormal); mTextPaintNormal.setColor(mTextColorNormal); mTextPaintNormal.setFlags(TextPaint.ANTI_ALIAS_FLAG); mTextPaintNormal.setTextAlign(Align.CENTER); mTextPaintFlag = new TextPaint(); mTextPaintFlag.setTextSize(mFlagTextSize); mTextPaintFlag.setColor(mFlagTextColor); mTextPaintFlag.setFlags(TextPaint.ANTI_ALIAS_FLAG); mTextPaintFlag.setTextAlign(Align.LEFT); mLinePaint = new Paint(); mLinePaint.setColor(mTextColorHighLight); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setStrokeWidth(lineWidth * mDensity); mShaderPaintTop = new Paint(); mShaderPaintBottom = new Paint(); } /** * 初始化矩形 */ private void initRects() { mTextBoundsHighLight = new Rect(); mTextBoundsNormal = new Rect(); mTextBoundsFlag = new Rect(); } /** * 测量文字边界 */ private void measureText() { /* * 保证不同长度的数值边界相同 * 例如"2014" 到 "0000". */ String text = String.valueOf(mEndNumber); int length = text.length(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < length; i++) { builder.append("0"); } text = builder.toString(); //会按严格按照Paint的样式,绘制出文字的边界,调用native层去测量 mTextPaintHighLight.getTextBounds(text, 0, text.length(), mTextBoundsHighLight); mTextPaintNormal.getTextBounds(text, 0, text.length(), mTextBoundsNormal); if (mFlagText != null) { mTextPaintFlag.getTextBounds(mFlagText, 0, mFlagText.length(), mTextBoundsFlag); } } @Override protected void onDraw(Canvas canvas) { //绘制背景 canvas.drawColor(mBackgroundColor); //绘制两条选中数上下的边条 canvas.drawLine(0, mHighLightRect.top, mWidth, mHighLightRect.top, mLinePaint); canvas.drawLine(0, mHighLightRect.bottom, mWidth, mHighLightRect.bottom, mLinePaint); //绘制右上角文字 if (mFlagText != null) { int x = (mWidth + mTextBoundsHighLight.width() + 6) / 2; canvas.drawText(mFlagText, x, mHeight / 2, mTextPaintFlag); } //绘制选项 for (int i = 0; i < mTextYAxisArray.length; i++) { if (mTextYAxisArray[i].mmIndex >= 0 && mTextYAxisArray[i].mmIndex <= mEndNumber - mStartNumber) { String text = null; if (mTextArray != null) {//是否自定义字符数组 text = mTextArray[mTextYAxisArray[i].mmIndex]; } else { text = String.valueOf(mNumberArray[mTextYAxisArray[i].mmIndex]); } canvas.drawText( text, mWidth / 2, mTextYAxisArray[i].mmPos + mTextBoundsNormal.height() / 2, mTextPaintNormal); } } // 绘制遮罩 canvas.drawRect(0, 0, mWidth, mHighLightRect.top, mShaderPaintTop); canvas.drawRect(0, mHighLightRect.bottom, mWidth, mHeight, mShaderPaintBottom); // Scroll the number to the position where exactly they should be. // Only do this when the finger no longer touch the screen and the fling action is finished. if (MotionEvent.ACTION_UP == mTouchAction && mFlingScroller.isFinished()) { adjustYPosition(); } } /** * 使数字滑动正确的位置(也就是说选中项要在正中间) */ private void adjustYPosition() { if (mAdjustScroller.isFinished()) { mStartY = 0; int offsetIndex = Math.round((float)(mTextYAxisArray[0].mmPos - mStartYPos) / (float)mSpacing); int position = mStartYPos + offsetIndex * mSpacing; int dy = position - mTextYAxisArray[0].mmPos; if (dy != 0) { mAdjustScroller.startScroll(0, 0, 0, dy, 300); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { // 父控件已经告诉picker要多宽 mWidth = widthSize; } else {//否则 //宽度=边条宽度+左内边距+右内边距+右上角文字宽度+6 mWidth = mTextBoundsHighLight.width() + getPaddingLeft() + getPaddingRight() + mTextBoundsFlag.width() + 6; } if (heightMode == MeasureSpec.EXACTLY) { // 父控件已经告诉picker要多高 mHeight = heightSize; } else {//否则 //高度=选项数目*高度+边条数目*选项垂直距离+上内边距+下内边距 mHeight = mLines * mTextBoundsNormal.height() + (mLines - 1) * mVerticalSpacing + getPaddingTop() + getPaddingBottom(); } setMeasuredDimension(mWidth, mHeight); /* * Do some initialization work when the view got a size */ if (null == mHighLightRect) { //RectF的坐标是用单精度浮点型表示的 mHighLightRect = new RectF();//表示整个picker mHighLightRect.left = 0; mHighLightRect.right = mWidth; mHighLightRect.top = (mHeight - mTextBoundsHighLight.height() - mVerticalSpacing) / 2; mHighLightRect.bottom = (mHeight + mTextBoundsHighLight.height() + mVerticalSpacing) / 2; //上遮罩 Shader topShader = new LinearGradient(0, 0, 0, mHighLightRect.top, new int[] { mBackgroundColor & 0xDFFFFFFF, mBackgroundColor & 0xCFFFFFFF, mBackgroundColor & 0x00FFFFFF }, null, Shader.TileMode.CLAMP); //下遮罩 Shader bottomShader = new LinearGradient(0, mHighLightRect.bottom, 0, mHeight, new int[] { mBackgroundColor & 0x00FFFFFF, mBackgroundColor & 0xCFFFFFFF, mBackgroundColor & 0xDFFFFFFF }, null, Shader.TileMode.CLAMP); mShaderPaintTop.setShader(topShader); mShaderPaintBottom.setShader(bottomShader); //选项高度=垂直距离+文字高度 mSpacing = mVerticalSpacing + mTextBoundsNormal.height(); //起始绘制Y坐标=控件高度/2-3*选项高度 mStartYPos = mHeight / 2 - 3 * mSpacing; //起始结束Y坐标=控件高度/2+3*选项高度 mEndYPos = mHeight / 2 + 3 * mSpacing; initTextYAxisArray(); } } /** * 初始化选项文字的Y坐标 */ private void initTextYAxisArray() { for (int i = 0; i < mTextYAxisArray.length; i++) { //保证选中项在正中间 NumberHolder textYAxis = new NumberHolder(mCurrNumIndex - 3 + i, mStartYPos + i * mSpacing); if (textYAxis.mmIndex > mNumberArray.length - 1) {//如果选中项之后不够3项,用头部补足 textYAxis.mmIndex -= mNumberArray.length; } else if (textYAxis.mmIndex < 0) {//如果选中项之前不够3项,用尾部补足 textYAxis.mmIndex += mNumberArray.length; } mTextYAxisArray[i] = textYAxis; } } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) {//是否enabled return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); //表示用于多点 触控检测点 int action = event.getActionMasked(); mTouchAction = action; if (MotionEvent.ACTION_DOWN == action) { mStartY = (int) event.getY(); if (!mFlingScroller.isFinished() || !mAdjustScroller.isFinished()) {//没有停止,强制停止 mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } else if (MotionEvent.ACTION_MOVE == action) { mCurrY = (int) event.getY(); mOffectY = mCurrY - mStartY; if (!mCanScroll && Math.abs(mOffectY) < mTouchSlop) { return false; } else { mCanScroll = true; if (mOffectY > mTouchSlop) { mOffectY -= mTouchSlop; } else if (mOffectY < -mTouchSlop) { mOffectY += mTouchSlop; } } mStartY = mCurrY; computeYPos(mOffectY); onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); invalidate(); } else if (MotionEvent.ACTION_UP == action) { mCanScroll = false; VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {//如果快速滑动 fling(initialVelocity); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else {//如果只是轻微移动 adjustYPosition(); invalidate(); } mVelocityTracker.recycle(); mVelocityTracker = null; } return true; } /** * 这个函数会在控件滚动的时候调用,准确来说,是在父控件调用drawchild()以后,在控件调用draw()以前 */ @Override public void computeScroll() { Scroller scroller = mFlingScroller; if (scroller.isFinished()) {//如果滚动已经停止了 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); scroller = mAdjustScroller;//使scroller为mAdjustScroller if (scroller.isFinished()) { return; } } scroller.computeScrollOffset(); mCurrY = scroller.getCurrY(); mOffectY = mCurrY - mStartY; computeYPos(mOffectY); invalidate(); mStartY = mCurrY; } /** * Make {@link #mTextYAxisArray} to be a circular array * 使mTextYAxisArray变成一个循环数组 * * @param offectY */ private void computeYPos(int offectY) { for (int i = 0; i < mTextYAxisArray.length; i++) { mTextYAxisArray[i].mmPos += offectY; if (mTextYAxisArray[i].mmPos >= mEndYPos + mSpacing) {//如果新位置超出高度 mTextYAxisArray[i].mmPos -= (mLines + 2) * mSpacing;//定位到开头 mTextYAxisArray[i].mmIndex -= (mLines + 2);//更新序号 if (mTextYAxisArray[i].mmIndex < 0) { mTextYAxisArray[i].mmIndex += mNumberArray.length; } } else if (mTextYAxisArray[i].mmPos <= mStartYPos - mSpacing) {//如果新位置超出起始点 mTextYAxisArray[i].mmPos += (mLines + 2) * mSpacing;//定位到结尾 mTextYAxisArray[i].mmIndex += (mLines + 2);//更新序号 if (mTextYAxisArray[i].mmIndex > mNumberArray.length - 1) { mTextYAxisArray[i].mmIndex -= mNumberArray.length; } } if (Math.abs(mTextYAxisArray[i].mmPos - mHeight / 2) < mSpacing / 4) {//离中间距离小于mSpacing / 4 mCurrNumIndex = mTextYAxisArray[i].mmIndex;//取得当前值 int oldNumber = mCurrentNumber; mCurrentNumber = mNumberArray[mCurrNumIndex]; if (oldNumber != mCurrentNumber) { if (mOnValueChangeListener != null) { mOnValueChangeListener.onValueChange(this, oldNumber, mCurrentNumber); } // Play a sound effect if (mSound != null && mSoundEffectEnable) { mSound.playSoundEffect(); } } } } } /** * 设置快速滑动fling * @param startYVelocity */ private void fling(int startYVelocity) { int maxY = 0; if (startYVelocity > 0) {//向下滑动 maxY = (int) (mTextSizeNormal * 10); mStartY = 0; mFlingScroller.fling(0, 0, 0, startYVelocity, 0, 0, 0, maxY); } else if (startYVelocity < 0) {//向上滑动 maxY = (int) (mTextSizeNormal * 10); mStartY = maxY; mFlingScroller.fling(0, maxY, 0, startYVelocity, 0, 0, 0, maxY); } invalidate(); } /** * 数字对象,保存有该数字所在的起始Y坐标,在当前显示项的index */ class NumberHolder { /** * Array index of the number in {@link #mTextYAxisArray} */ public int mmIndex; /** * 该数字的起始Y坐标 */ public int mmPos; public NumberHolder(int index, int position) { mmIndex = index; mmPos = position; } } public void setStartNumber(int startNumber) { mStartNumber = startNumber; verifyNumber(); initTextYAxisArray(); invalidate(); } public void setEndNumber(int endNumber) { mEndNumber = endNumber; verifyNumber(); initTextYAxisArray(); invalidate(); } public void setCurrentNumber(int currentNumber) { mCurrentNumber = currentNumber; verifyNumber(); initTextYAxisArray(); invalidate(); } public int getCurrentNumber() { return mCurrentNumber; } /** * Interface to listen for changes of the current value. * 值改变监听接口 */ public interface OnValueChangeListener { /** * Called upon a change of the current value. * * @param picker The NumberPicker associated with this listener. * @param oldVal The previous value. * @param newVal The new value. */ void onValueChange(NumberPicker picker, int oldVal, int newVal); } /** * Interface to listen for the picker scroll state. * 滑动监听接口 */ public interface OnScrollListener { /** * The view is not scrolling. * 没有滑动 */ public static int SCROLL_STATE_IDLE = 0; /** * The user is scrolling using touch, and their finger is still on the screen. * 因手指触摸而滑动 */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; /** * The user had previously been scrolling using touch and performed a fling. * 手指已经离开屏幕时,继续滑动 */ public static int SCROLL_STATE_FLING = 2; /** * Callback invoked while the number picker scroll state has changed. * * @param view The view whose scroll state is being reported. * @param scrollState The current scroll state. One of * {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_TOUCH_SCROLL} or * {@link #SCROLL_STATE_IDLE}. */ public void onScrollStateChange(NumberPicker picker, int scrollState); } /** * 设置滑动监听 * @param l */ public void setOnScrollListener(OnScrollListener l) { mOnScrollListener = l; } /** * 设置值改变监听 * @param l */ public void setOnValueChangeListener(OnValueChangeListener l) { mOnValueChangeListener = l; } /** * 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); } } /** * 设置音效 * @param sound */ public void setSoundEffect(Sound sound) { mSound = sound; } @Override /** * 设置音效功能是否开启 */ public void setSoundEffectsEnabled(boolean soundEffectsEnabled) { super.setSoundEffectsEnabled(soundEffectsEnabled); mSoundEffectEnable = soundEffectsEnabled; } /** * Display custom text array instead of numbers * 使用自定义的数组来展示选项 * @param textArray */ public void setCustomTextArray(String[] textArray) { mTextArray = textArray; invalidate(); } }
chenglei1986/DatePicker源码解析(一)
标签:
原文地址:http://blog.csdn.net/crazy__chen/article/details/45936957