很早就想来完善之前写的走势图(一)了,结果两个月感觉天天都在忙,无脑写代码,还偶尔通宵,尼玛啊,一脸要死的样子,现在总算有点时间了~~~~
这篇是在第一篇的基础上进行的完善和改进,主要就是加入了动画和触摸效果,效果图如下:
在之前的基础上,首先改善的第一点就是重新计算了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() {@Overridepublic 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() {@Overridepublic 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()方法:
@Overridepublic 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);}
到这里,最开始图上的效果就基本实现了