很早就想来完善之前写的走势图(一)了,结果两个月感觉天天都在忙,无脑写代码,还偶尔通宵,尼玛啊,一脸要死的样子,现在总算有点时间了~~~~
这篇是在第一篇的基础上进行的完善和改进,主要就是加入了动画和触摸效果,效果图如下:
在之前的基础上,首先改善的第一点就是重新计算了x轴和y轴文字的坐标,让这些文字显示出来的位置更准确,主要就是计算了这些文字的宽高,然后根据这些文字的宽高来计算它们相对x轴和y轴的偏移量,改进后的代码如下:
Paint.FontMetrics fm = textPaint.getFontMetrics();
if(isShowXText){ //绘制x轴文字
float h1 = fm.descent - fm.ascent; //x轴文字高度,相对x轴向下偏移的值
for(int x1 = 0;(x1 * 4) < points.size();x1++){
String xStr = points.get(x1 * 4).getDate();
float xw = textPaint.measureText(xStr); //x轴文字的宽度,为了计算文字宽度的中心点
canvas.drawText(points.get(x1 * 4).getDate(), left + xDivider * x1 - xw/2,bottom + h1, textPaint);
}
}
if(isShowYText){ //绘制y轴数值
float h2 = Math.abs(fm.descent + fm.ascent); //y轴文字相对于文字baseline的高度,为了计算高度的中心点
float avIncome = maxPoint.getIncome() / 5;
for(int y1 = 1;y1 <=5 ;y1++){
String income = new DecimalFormat("0.00").format(avIncome * y1);
float textWidth = textPaint.measureText(income); //y轴文字宽度,相对y轴向左偏移的值
canvas.drawText(income,left - textWidth - 5,bottom - yDivider * y1 + h2/2, textPaint);
}
}
在上面代码中计算文字宽高,为了保证横竖刻度线和文字的中心点是对齐的(强迫症!!!???)
接下来要做的就是加入x和y方向上的动画效果,这里就用属性动画来实现这种效果:
private float phaseY = 1f;
private int xCount = 0;
首先定义的这两个属性,就是要通过属性动画来改变的两个值,xCount是用来控制点的个数,phaseY用来控制所有点的y值的变化,主要在描点的时候来控制:
private void drawDataLines(Canvas canvas, List<Point> points){
for(int i=0;i<xCount;i++){
Point point1 = points.get(i == 0? 0: i-1);
Point point2 = points.get(i);
Location location1 = getLocation(point1,i == 0?0: i-1 );
Location location2 = getLocation(point2, i);
canvas.drawCircle(location2.x, location2.y, 5f, textPaint);
if(i > 0){
canvas.drawLine(location1.x, location1.y, location2.x,location2.y, linePaint);
}
}
}
private Location getLocation(Point point, int n){
if(specY == 0 || specX == 0){
specY = (bottom - top) / maxPoint.getIncome();
specX = (right - left) / (points.size() - 1);
}
float x = n * specX + left;
float y = bottom - point.getIncome() * specY * phaseY;
return new Location(x, y);
}
上面的drawDataLines()方法就是描点连线的方法,用xCount来控制for循环的最大值,也就控制了需要描点的数量;getLocation()方法对点的一个转换的方法,里面对获取点的y值的时候加入了phaseY,来控制y值的变化,然后就是实现两个属性动画:
public void animX(long duration){
ObjectAnimator animator = ObjectAnimator.ofInt(this, "xCount", 0, points.size());
animator.setDuration(duration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
});
animator.start();
}
public void animY(long duration){
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "phaseY", 0f, 1f);
animator.setDuration(duration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
});
animator.start();
}
可以看到,animX()方法我用的ofInt来控制的,因为点的个数只能是整数,这样就行了;而animY()方法中,用了ofFloat来控制,让phaseY用0变到1,这样来实现所有点的y值从0变到原值,这里要注意的是对于这两个属性一定要定义set()方法,因为ObjectAnimator是通过反射来调用属性的set方法来赋值的。这个时候你就可以在外面调用这两个方法了,去体验一下流畅顺滑的动画效果(还有一点要注意的是,执行动画的时候会频繁调用invalidate(),所以在执行OnDraw()之前,如果有用到path来存放线条,一定要记得reset,不然执行动画的是时候,点越多,就会越卡)~~~~~
最后要实现的就是手触摸屏幕时,显示出手触摸在哪个点上,并通过指示线显示出具体的值,先来定义一些要用到的关键属性:
private Paint indicatorPaint; //指示线画笔
private Paint indicateValuePaint; //绘制x和y值画笔
private Paint indicateRectPaint; //绘制x和y值显示区域的画笔
private boolean isTouch = false;
private float[] indicates = new float[8]; //横竖指示线的坐标
private RectF rectY; //显示y值的矩形区域
private RectF rectX; //显示x值的矩形区域
private Point touchedPoint; //触摸到的点
private Location xValueLocation; //绘制x值的坐标
private Location yValueLocation; //绘制y值的坐标
对于这些属性的初始化就不放上来了,这里的关键就是手指触摸时坐标的计算,下面是主要计算代码:
private void setIndicators(float x, float y){
if(x < left){
x = left;
}
if(x > right){
x = right;
}
int position = new BigDecimal((x-left)/specX).setScale(0,BigDecimal.ROUND_HALF_UP).intValue();
Point point = points.get(position);
touchedPoint = point;
Location location = getLocation(point, position);
//计算手指触摸时,两条指示线的坐标并赋值
indicates[0] = left;
indicates[1] = location.y;
indicates[2] = right;
indicates[3] = location.y;
indicates[4] = location.x;
indicates[5] = top;
indicates[6] = location.x;
indicates[7] = bottom;
Paint.FontMetrics fm = indicateValuePaint.getFontMetrics();
//计算两条指示线在坐标轴上显示值的矩形区域
String date = point.getDate();
String income = new DecimalFormat("0.00").format(point.getIncome());
float dateWidth = indicateValuePaint.measureText(date);
float incomeWidth = indicateValuePaint.measureText(income);
float height = fm.bottom - fm.top;
//绘制x值的矩形区域
rectX.left = location.x - dateWidth/2 - 5;
rectX.top = bottom;
rectX.right = location.x + dateWidth/2 + 5;
rectX.bottom = bottom + height;
//绘制y值的矩形区域
rectY.left = left - incomeWidth - 10;
rectY.top = location.y - height/2;
rectY.right = left;
rectY.bottom = location.y + height/2;
//计算绘制x,y值的坐标
xValueLocation.x = rectX.left + 5;
xValueLocation.y = rectX.bottom - fm.descent;
yValueLocation.x = rectY.left + 5;
yValueLocation.y = rectY.bottom - fm.descent;
}
关键的计算在代码都给出了注释,方法中传进来的x,y就是手指触摸时的屏幕上的坐标,这里对手指触摸是做了一个判断,离哪个点更近,哪个点就是被触摸的那个点。接着就是重写该view 的onTouchEvent()方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isTouch = true;
setIndicators(x, y);
break;
case MotionEvent.ACTION_MOVE:
setIndicators(x, y);
break;
case MotionEvent.ACTION_UP:
isTouch = false;
break;
}
invalidate();
return true;
}
在Down和Move的事件中,调用了上面的setIndicators()的方法,需要的东西都准备完了,接着要来根据这些值画出指示线和值了了:
//绘制手触摸的时候的指示线和对应的值
private void drawIndicateLines(Canvas canvas){
canvas.drawLines(indicates, indicatorPaint);
canvas.drawRect(rectX, indicateRectPaint);
canvas.drawRect(rectY, indicateRectPaint);
canvas.drawText(touchedPoint.getDate(), xValueLocation.x, xValueLocation.y, indicateValuePaint);
String income = new DecimalFormat("0.00").format(touchedPoint.getIncome());
canvas.drawText(income, yValueLocation.x, yValueLocation.y, indicateValuePaint);
}
上面这个方法中,draw方法绘制的内容由上到下依次为:
①绘制两条指示线;②绘制x值显示的矩形背景;③绘制y值显示的矩形背景;④绘制x值;⑤绘制y值。
最后再onDraw中调用:
if(isTouch){
drawIndicateLines(canvas);
}
到这里,最开始图上的效果就基本实现了