自定义View实现跟随手指滚动的刻度尺,实现了相似SeekBar的滑动选中效果。项目地址,欢迎star!java
UI图:git
功能:github
先扯一下,再看别人写的控件的时候总有一种一脸懵逼的感受,好多凌乱的变量和一大堆的计算逻辑都不知道干吗用的。好比:PullToRefreshLayout。除非本身按着总体的设计流程写一遍,一步步的写,等出了bug你就明白那些操做的价值。结合以前读第三方控件的经验,写这个刻度尺控件的时候就一步步的去完成,从简单的绘制,到点击事件,再到滑动fling,最后滑动结束更正滑动位置。每一步遇到的问题都记录下来,以后再补全解决方法,这就是成长。json
这里省略了onMeasure,这里的需求只是计算一下高度就行了。接着看onDraw方法:canvas
private void drawRuler(Canvas canvas) {
mTextIndex = 0;
for (int index = 0; index <= mRulerHelper.getCounts(); index++) {
boolean longLine = mRulerHelper.isLongLine(index);
int lineCount = mLineWidth * index;
mRect.left = index * mLineSpace + lineCount + mMarginLeft;
mRect.top = getStartY(longLine);
mRect.right = mRect.left + mLineWidth;
mRect.bottom = getEndY();
if (longLine) {
if (!mRulerHelper.isFull()) {
mRulerHelper.addPoint(mRect.left);
}
String text = mRulerHelper.getTextByIndex(mTextIndex);
mTextIndex++;
canvas.drawText(text, mRect.centerX(), getMeasuredHeight() - dpFor14, mTextPaint);
}
canvas.drawRect(mRect, mLinePaint);
mRect.setEmpty();
}
}
复制代码
这里解释一下为何刻度采用Rect而不是设置line的宽度,其实最简单的就是设置Paint的宽度而后canvas.drawLine()。刚绘制的时候就是采用的canvas.drawLine(),绘制完以后发现每一个刻度的宽度都被削减了一半,canvas.drawLine()是在设置的(x,y)坐标开始平分line的宽度的(这个你要去体验一下就会明白)。因此给定坐标以后每一个刻度看起来就像是被挤了同样,因此才采用Rect简单方便一点。进入正题,绘制有几个问题:ide
怎么肯定要绘制几个Rect?学习
这个比较灵活,要看具体的需求了。也就是一大格里面包含几个刻度,通常是包含10个刻度,刻度包括长短刻度。而后一大格刻度表示多少数值,也就是offSet值是多少。以后刻度的范围也要明确而且能被offSet整除,好比范围是(low,height),那么(height-low)/(offSet/10)就是你须要绘制多少个刻度。this
public void setScope(int start, int count,int offSet) {
if(offSet != 0) {
this.offSet = offSet;
}
lineNumbers = (count - start) / (this.offSet / 10);
}
复制代码
怎么肯定那个是长刻度?spa
这个问题要肯定一大格之间有几个小刻度了,通常为10个的话,那么当前的index/10能整除就是到了该绘制长刻度的时候了,mRulerHelper.getCounts()就是咱们计算出的总共有几个刻度。.net
for (int index = 0; index <= mRulerHelper.getCounts(); index++) {
boolean longLine = mRulerHelper.isLongLine(index);
...
if (longLine) {
canvas.drawText(text, mRect.centerX(), getMeasuredHeight() - dpFor14, mTextPaint);
}
canvas.drawRect(mRect, mLinePaint);
}
复制代码
以后呢就是咱们计算Rect的左边跟绘制Text的坐标了。。。不细讲。。。具体可看这里啊。
有个问题就是你得明白Rect的left top right bottom分别表示那个区间:
目前采起的是点击该View的事件全拦截,感受也没别的什么需求须要过滤事件了。事件处理起来很简单的就是计算出每次移动的差值就行了:
case MotionEvent.ACTION_DOWN:
mPressUp = false;
isFling = false;
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
mPressUp = false;
float distance = event.getX() - startX;
if (mPreDistance != distance) {
doScroll((int) -distance, 0, 0);
invalidate();
}
startX = event.getX();
break;
复制代码
问题就是:
怎么实现滑动的效果?
刻度尺若是范围很大的话总宽度确定会超出屏幕的,可是Canvas不会绘制屏幕以外的部分,除非等到屏幕以外的部分显示出来。另外让View滑动的方法不少,最初使用的是scrollTo方法,该方法滑动的是View的内容,也符合咱们要的效果,不过结果查强人意。差值计算以后稍微一滑动,刻度直接没了,成了一片空白,看起来那个变化值也不大,ok!这是一个疑问ScrollTo+invalidate内容不会显示,直接没了。以后呢换成了Scroller,这个玩意不用太多的介绍了,使用以后便达到了咱们想要的效果,同样的变化值。
private void doScroll(int dx, int dy, int duration) {
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
}
复制代码
是否有疑问?既然屏幕以外的东西Canvas不会去绘制,那么滑动的时候确定是将屏幕以外的部分滑到屏幕中,也就是在滑动的过程当中要继续绘制。从上面的绘制代码能看到这个绘制过程当中跟滑动并无任何的联系,只是单纯的for循环绘制而已,为何呢?第一 咱们scrollTo移动的是View的内容,一开始View的实际宽度会超过屏幕的宽度,当没有滑动的时候,View只会绘制屏幕中的可见区域,即便for循环依然执行也不会绘制到屏幕外面,而后在滑动的时候会不断的触发invalidate()方法,也就是for循环会被触发,View开始在新出现的未绘制的区域绘制。已经绘制过的区域会被滑出屏幕,这样就会给用户一个平滑的效果。作完以上两步你的刻度尺已经有了滑动的效果了。下面就是解决边界的问题。
UI说当超过边界以后松手回弹,这样的交互效果好。这种交互其实最简单了,在手指离开的时候计算当前的x坐标距离中心指针的x坐标的距离,而后让Scroller去执行回弹的效果。不过这个操做是整个控件中最为重要的一步,由于当手指抬起的时候,中间指针必须指向一个长刻度,不能停留再短刻度上面,那这个操做就跟边界回弹的操做重合了,边界回弹也是让最小或者最大长刻度滑动到中间指针的位置。因此松手以后的操做就分为三种:
currentX :滑动中止时的x坐标。
Point:中间指针位置。
low:刻度尺的最小边界。
height:刻度尺的最大边界。
当前的currentX小于中间指针刻度Point的x坐标,而且小于刻度的最小值low的x坐标。
-----------------Point-currentX--low------height----------
当前的currentX小于中间指针刻度Point的x坐标,而且大于刻度的最小值low表示的x坐标小于刻度尺的最大刻度height的x坐标。
------low-------currentX--Point--------height----------
当前的currentX大于中间指针刻度Point的x坐标,而且大于刻度的最大值height表示的x坐标。
------low-------height-----currentX-Point-------
简单的表示了一下三种位置。
处理就是,先计算出滑动结束以后的当前x坐标跟中间Point的x坐标的距离,而后不为0就使用Scroller滑动:
//计算距离
public int getScrollDistance(int x) {
for (int i = 0; i < mPoints.size(); i++) {
int pointX = mPoints.get(i);
if (0 == i && x < pointX) {
//当前的x比第一个位置的x坐标都小 也就是须要往右移动到第一个长线的位置.
setCurrentText(0);
return x - pointX;
} else if (i == mPoints.size() - 1 && x > pointX) {
//当前的x比最后一个左边的x都大,也就是须要往左移动到最后一个长线位置.
setCurrentText(texts.size() - 1);
return x - pointX;
} else {
if (i + 1 < mPoints.size()) {
int nextX = mPoints.get(i + 1);
if (x > pointX && x <= nextX) {
int distance = (nextX - pointX) / 2;
int dis = x - pointX;
if (dis > distance) {
//说明往下一个移动
setCurrentText(i + 1);
return x - nextX;
} else {
setCurrentText(i);
//往前一个移动
return x - pointX;
}
}
}
}
}
return 0;
}
复制代码
开始执行滑动:
public void scrollFinish() {
int finalX = mScroller.getFinalX();
int centerPointX = mRulerHelper.getCenterPointX();
int currentX = centerPointX + finalX;
int scrollDistance = mRulerHelper.getScrollDistance(currentX);
if (0 != scrollDistance) {
//第一个参数是滚动开始时的x的坐标
//第二个参数是滚动开始时的y的坐标
//第三个参数是在X轴上滚动的距离, 负数向右滚动.
//第四个参数是在Y轴上滚动的距离,负数向下滚动.
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -scrollDistance, 0, 300);
invalidate();
if (scrollSelected != null) {
scrollSelected.selected(getCurrentText());
}
}
}
复制代码
这样已经可使用了,滑动的刻度尺已经完成了。不过交给UI一看,人家说这东西怎么那么难滑动呢,每次怎么只能滑一大格呢,我要那种fling的感受。确实,由于在MotionEvent.ACTION_UP的时候都会去矫正一下位置,因此给使用者的感受就是一次只能滑一格,滑动体验很很差,只能去增长fling。。。
增长fling多简单啊,Scroller不是有这个方法吗mScroller.fling(),使用方法这里再也不介绍了。fling增长以后,用户的体验确实好了不少,不过一个新的问题出现了,就是在fling中止以后怎么矫正位置呢?这是个大问题,卡住了好大一下子,最终找到了解决方法:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
//这里是结束以后调用矫正位置的方法。scrollFinish()。
if (mScroller.getCurrX() == mScroller.getFinalX() && mPressUp && isFling) {
mPressUp = false;
isFling = false;
scrollFinish();
}
scrollTo(mScroller.getCurrX(), 0);
invalidate();
}
super.computeScroll();
}
复制代码
效果在文章一开始已经展现出来了,指针并无在该自定义View中绘制,底部的线也是,由于对于指针的需求是多变的,因此用了一个自定义的ViewGroup去完成剩余的指针和底部的实线。底部的实线放在Group中是由于咱们的UI效果,底部的实线上面能够没有刻度,也就是这个底部的线是固定在底部,比我画在刻度下面跟随刻度滑动要简单的多。想到以后的变体,感受刻度自己的View跟指针分开是比较好扩展的,Group只须要给刻度尺控件传入中间指针的(x,y)坐标就行了。