最近发现一个控件wheelview如图:
顾名思义,滚轮控件,专门是用来做日期控件的,有了它任何日期控件都不再害怕
,而且这个控件很小只有两个类,自己在其基础上自定义样式很方便。我看见小的控件,就忍不住想分析。
下面介绍它所走的大的流程:
初始化的时候调用:
public void setWheelStyle(int style) {
itemList = WheelStyle.getItemList(context, style);
if (itemList != null) {
itemCount = itemList.size();
resetCurrentSelect();
invalidate();
} else {
Log.i(TAG, "item is null");
}
}
可以设置滚轮控件的条目信息,可以是年月日 时分秒 一个滚轮控件对应一个年或者月,或者时分秒。
接着调用onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.e("gac","onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mViewHeight = getMeasuredHeight();
int mViewWidth = getMeasuredWidth();
centerX = (float) (mViewWidth / 2.0);
centerY = (float) (mViewHeight / 2.0);
isInit = true;
invalidate();
}
测量控件的高度和宽度,以及中间的纵坐标值。
当你拖动控件的时候
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
doDown(event);
break;
case MotionEvent.ACTION_MOVE:
doMove(event);
break;
case MotionEvent.ACTION_UP:
doUp();
break;
default:
break;
}
return true;
}
- 会依次调用doDown doMove 和doUp程序的结构还是很清晰的。
下面我们从他的一个字段说起:mMoveLen,也就是滑动的距离。
首先看一下doMove方法:
private void doMove(MotionEvent event) {
//event.getY view 相对于父布局的左上角的纵坐标
//mMoveLen<0 向上滑动 mMoveLen>0向下滑动
mMoveLen += (event.getY() - mLastDownY);
Log.e("gac","----doMove-----mMoveLen:"+mMoveLen+" itemHeight/2:"+itemHeight/2);
if (mMoveLen > itemHeight / 2) {
// 往下滑超过离开距离
mMoveLen = mMoveLen - itemHeight;
currentItem--;
Log.e("gac","downmove: moveLen:"+mMoveLen+" currentItem:"+currentItem);
if (currentItem < 0) {
currentItem = itemCount - 1;
}
} else if (mMoveLen < -itemHeight / 2) {
// 往上滑超过离开距离
mMoveLen = mMoveLen + itemHeight;
currentItem++;
Log.e("gac","upmove: moveLen:"+mMoveLen+" currentItem:"+currentItem);
if (currentItem >= itemCount) {
currentItem = 0;
}
}
mLastDownY = event.getY();
invalidate();
}
- 我们接下来只会分析向下滑动的一种情况:
mMoveLen 移动的距离 < 0 向上滑动 >0 向下滑动
if (mMoveLen > itemHeight / 2) {
// 往下滑超过离开距离
mMoveLen = mMoveLen - itemHeight;
currentItem--;
Log.e("gac","downmove: moveLen:"+mMoveLen+" currentItem:"+currentItem);
if (currentItem < 0) {
currentItem = itemCount - 1;
}
}
- mMoveLen 移动的距离大于中间text 二分之一的距离的时候, 假设mMoveLen = 1/2*ItemHeight
因为向下滑动 mMoveLen 是正值,mMoveLen = mMoveLen - itemHeight; 此时mMoveLen应该为负值 -1/2*ItemHeight
因为向下滑动,显示的为上一条数据所以currentItem-1.
为什么让 mMoveLen = mMoveLen - itemHeight; 得到一个负值呢?假设我们松开手,看看程序如何走?
此时执行doUP函数
private void doUp() {
// 抬起手后mCurrentSelected的位置由当前位置move到中间选中位置
if (Math.abs(mMoveLen) < 0.0001) {
mMoveLen = 0;
return;
}
if (mTask != null) {
mTask.cancel();
mTask = null;
}
if (null == timer) {
timer = new Timer();
}
mTask = new MyTimerTask(updateHandler);
timer.schedule(mTask, 0, 10);
}
- doup函数执行了一个定时器,看看定时器是干什么用的。
Handler updateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (Math.abs(mMoveLen) < SPEED) {
// 如果偏移量少于最少偏移量
mMoveLen = 0;
if (null != timer) {
timer.cancel();
timer.purge();
timer = null;
}
if (mTask != null) {
mTask.cancel();
mTask = null;
performSelect();
}
} else {
// 这里mMoveLen / Math.abs(mMoveLen)是为了保有mMoveLen的正负号,以实现上滚或下滚
mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;
}
invalidate();
}
};
最终它会执行mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED; 不管moveLen为正值还是负值,
此时mMoveLen 的值都会等于0 ,这个定时器让mMoveLen 的值越来越小最后为0,并且不断调用invalidate()函数
这样再mMoveLen变小的过程中就会不断去执行 onDraw()在onDraw方法里执行下面这个函数
private void drawData(Canvas canvas) {
// 先绘制选中的text再往上往下绘制其余的text
if (!itemList.isEmpty()) {
// 绘制中间data
drawCenterText(canvas);
// 绘制上方data
for (int i = 1; i < SHOW_SIZE + 1; i++) {
drawOtherText(canvas, i, -1);
}
// 绘制下方data
for (int i = 1; i < SHOW_SIZE + 1; i++) {
drawOtherText(canvas, i, 1);
}
}
}
- 我们先看drawCenterText函数
private void drawCenterText(Canvas canvas) {
// text居中绘制,注意baseline的计算才能达到居中,y值是text中心坐标
Log.e("gac","drawCenterText centerY:"+centerY+" mMoveLen:"+mMoveLen);
float y = centerY + mMoveLen;
FontMetricsInt fmi = selectPaint.getFontMetricsInt();
float baseline = (float) (y - (fmi.bottom / 2.0 + fmi.top / 2.0));
Log.e("gac","drawCenterText: basLine:"+baseline);
canvas.drawText(itemList.get(currentItem), centerX, baseline, selectPaint);
}
- 我们来捋一捋,刚开始我们向下滑动,mMoveLen 为负值 当等于 1/2*ItemHeight 执行了,mMoveLen=mMoveLen-itemHeight
mMoveLen = -1/2*ItemHeight,我们松手以后,执行了doUP函数,执行一个定时器,定时器让mMoveLen的值从-1/2*ItemHeight
逐渐变为0,这个过程不断去执行drawCenterText 此时 y值越来越大,basLine的值也会越来越大,当mMovenLen =0
的时候,y = centerY basLine = centerY - (fmi.bottom / 2.0 + fmi.top / 2.0));
也就是说baseLine的值从小变大,最后到中间位置默认的baseline,中间的文本从偏上的距离慢慢回到中间位置
也是向下滑动的。
下面看看如何画上方文本:
// 绘制上方data
for (int i = 1; i < SHOW_SIZE + 1; i++) {
drawOtherText(canvas, i, -1);
}
SHOW_SIZE = 1 所以这个循环只会执行一次也就是 i = 1,SHOW_SIZE 参数其实就是上方显示的文本数.
这个函数就是drawOtherText(canvas, 1, -1);
private void drawOtherText(Canvas canvas, int position, int type) {
int index = currentItem + type * position;
if (index >= itemCount) {
index = index - itemCount;//当index >= itemCount //此时index就会从尾部回到头部,达到循环显示作用
}
if (index < 0) {
index = index + itemCount;
}
String text = itemList.get(index);
int itemHeight = getHeight() / (SHOW_SIZE * 2 + 1);
float d = itemHeight * position + type * mMoveLen;
float y = centerY + type * d;
FontMetricsInt fmi = mPaint.getFontMetricsInt();
float baseline = (float) (y - (fmi.bottom / 2.0 + fmi.top / 2.0));
canvas.drawText(text, centerX, baseline, mPaint);
}
看一下index = currentItem + type * position;其实就是index = currentItem -1
获得的文本就是上一个文本, 上方文本的中间位置 应该等于 centerY-ItemHeight;
上方文本的基线等于(centerY-ItemHeight)-(fmi.bottom / 2.0 + fmi.top / 2.0))
mMoveLen : -1/2*ItemHeight ->0
d: 3/2*ItemHeight->itemHeight
y: centerY-3/2*ItemHeight -> centerY-ItemHeight => 0 -> 1/2*ItemHeight,
上方文本的基线也是逐渐变大的 所以 仍然是向下滑动到上方文本的中间位置drawOhterText(canvas,1,1)绘制下方文本的时候
index = currentItem+1; 获取下方文本的内容
我们看看参数如何变化
mMoveLen : -1/2*ItemHeight ->0
d: 1/2*ItemHeight->itemHeight
y: centerY+1/2*ItemHeight -> centerY+ItemHeight => 2*ItemHeight -> 5/2*ItemHeight,
y值越来越大 下方文本也是基线越来越大 下方文本滑动的下方的基线位置停止简单的分析这一种情况得到的结果竟然是上中下文本都会向下滑动到所在的基线位置。
这个我感觉是特别绕,但是,作者还是牛逼的给写出来了。功底真是深厚。最后附上原作者的项目地址:猛戳这里