码迷,mamicode.com
首页 > 其他好文 > 详细

chenglei1986/DatePicker源码解析(一)

时间:2015-05-23 22:47:54      阅读:329      评论:0      收藏:0      [点我收藏+]

标签:

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

对于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()方法

@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();		
		}
	}

这个函数有些复杂,仔细看我们都计算出了些什么属性。我们根据选项的数目,选项之间的空隙,计算出整个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前面)

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

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

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 / 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();
					}
				}
			}
		}
	}
具体做法如上,对于每个选项,我们计算出它的新位置以后,如果已经超出了界限,就需要考虑是否变更它所代表的序号
我们还可以看到选项滑动时,有些接口的调用,显然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()以前
	 */
	@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;
	}

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

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!