概述
转载注明出处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;//获取当前值的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源码解析(一)的全部内容,希望文章能够帮你解决chenglei1986/DatePicker源码解析(一)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复