chenglei1986/DatePicker源码解析(一)

来源:互联网 发布:手机淘宝内置安全密码 编辑:程序博客网 时间:2024/06/07 06:10

转载注明出处http://blog.csdn.net/crazy__chen/article/details/45936957

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;

首先我们需要可以自定义numberpicker的样式,我们通过attr文件自定义自己的属性就可以了

<?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;//获取当前值的indexmTextYAxisArray = 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];

对于NumberHolder类,我们下面会看到,但是为什么是mLines+4呢,mLines是每次选项显示的数目,例如上面的图片里面,就是3,而加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);}}

这个函数比较重要,首先它注意到选项文字的统一问题,例如从1-12,我们更希望显示的是01,02,03······,10,11,12这样整齐等格式。

这个函数计算出最长的文字长度(稍后在画图时会用到),另外还是讲每个选项中,整个字符串的长宽,保留在了Rect中(其实前面定义的Rect就是用来保存长宽等属性的,当然我们也可以设置一下属性值来代替rect,但是使用Rect来记录,无疑更方便)


初始化完毕,然后是控件的绘制,对于绘制,我们先来看onMeasure()方法

@Overrideprotected 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();//表示整个pickermHighLightRect.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();}}

这个函数有些复杂,仔细看我们都计算出了些什么属性。我们根据选项的数目,选项之间的空隙,计算出整个numberpicker的高度(从这个计算我们也可以看出,条边是不独立占据高度的)。

另外还有上下遮罩的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前面)

获得高度等绘制的必要信息以后,我们开始绘制

@Overrideprotected 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()方法

@Overridepublic 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;}

VelocityTracker用于跟踪滑动速度

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 / 4mCurrNumIndex = mTextYAxisArray[i].mmIndex;//取得当前值int oldNumber = mCurrentNumber;mCurrentNumber = mNumberArray[mCurrNumIndex];if (oldNumber != mCurrentNumber) {if (mOnValueChangeListener != null) {mOnValueChangeListener.onValueChange(this, oldNumber, mCurrentNumber);}// Play a sound effectif (mSound != null && mSoundEffectEnable) {mSound.playSoundEffect();}}}}}
具体做法如上,对于每个选项,我们计算出它的新位置以后,如果已经超出了界限,就需要考虑是否变更它所代表的序号
我们还可以看到选项滑动时,有些接口的调用,显然mOnValueChangeListener是一个内部接口,用于回调通知值的改变,在最后贴出的完整代码中大家可以看到

另外还有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()以前 */@Overridepublic void computeScroll() {Scroller scroller = mFlingScroller;if (scroller.isFinished()) {//如果滚动已经停止了onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);scroller = mAdjustScroller;//使scroller为mAdjustScrollerif (scroller.isFinished()) {return;}}scroller.computeScrollOffset();mCurrY = scroller.getCurrY();mOffectY = mCurrY - mStartY;computeYPos(mOffectY);invalidate();mStartY = mCurrY;}

OK,到目前为止,整个控件的必要代码我们都看过了,只要思路清晰,一步一步的执行,就可以明白代码为什么要这样写,每一个函数存在的原因,这也是我们学习源码希望达到的目的。

最后,贴出源码文件下载地址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;//获取当前值的indexmTextYAxisArray = 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);}}@Overrideprotected 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);}}}@Overrideprotected 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();//表示整个pickermHighLightRect.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;}}@Overridepublic 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()以前 */@Overridepublic void computeScroll() {Scroller scroller = mFlingScroller;if (scroller.isFinished()) {//如果滚动已经停止了onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);scroller = mAdjustScroller;//使scroller为mAdjustScrollerif (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 / 4mCurrNumIndex = mTextYAxisArray[i].mmIndex;//取得当前值int oldNumber = mCurrentNumber;mCurrentNumber = mNumberArray[mCurrNumIndex];if (oldNumber != mCurrentNumber) {if (mOnValueChangeListener != null) {mOnValueChangeListener.onValueChange(this, oldNumber, mCurrentNumber);}// Play a sound effectif (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();    }}

0 0
原创粉丝点击