1. 介绍
了解更多请看:
LeafChart-实现自己的小型图表库(1)
LeafChart-实现自己的小型图表库(2)
LeafChart(3)-绘制直方图
LeafChart已经支持曲线图和直方图了,现在想升级一下,比如说来个动画绘制啊。之前使用过HelloChart的曲线图,它的动画效果是这样的
本来想借鉴一下动画效果的实现,可是我想要的动画效果不是这样子。
先睹为快
我想要的动画效果就如上图:
1. 从左向右逐渐绘制线条
2. 如果设置填充,则填充效果也要和线条绘制同步
2.实现原理
2.1效果1实现原理
一开始这个动画的实现方法真是想不到,还好在网上有大侠介绍思路。
参考博客:
使用DashPathEffect绘制一条动画曲线
这个里面介绍的方法是使用PathEffect子类DashPathEffect来绘制一条动画曲线。
DashPathEffect
DashPathEffect是PathEffect类的一个子类,可以使paint画出类似虚线的样子,并且可以任意指定虚实的排列方式。
举个例子:
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Style.STROKE);
p.setColor(Color.WHITE);
p.setStrokeWidth(1);
PathEffect effects = new DashPathEffect(new float[] { 1, 2, 4, 8}, 1);
p.setPathEffect(effects);
canvas.drawLine(0, 40, mWidth, 40, p);
代码中的float数组,必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白。
如本代码中,绘制长度1的实线,再绘制长度2的空白,再绘制长度4的实线,再绘制长度8的空白,依次重复。1是起始位置的偏移量。
效果如下:
技巧然而这跟我们绘制跟踪效果有什么关系呢?
看看这样一个PathEffect:
PathEffect effect = new DashPathEffect(new float[] { length, length }, 0);
我们可以把DashPathEffect的第一个参数(float数组)只填入两个值,都是path的总长度length,那么按照上面对DashPathEffect的解释,第一次绘制一条实线就已经完全绘制完了,间隔的空白区间得不到绘制的机会。事实上这样绘制完全不能产生虚线效果,跟不设置PathEffect是一样的。
但是我们注意第三个参数即起始位置的偏移量现在是为0的。如果我们不为0呢?
比如为100,那么第一次绘制实线就会跳过100的距离,第一次的实线就只能绘制length-100的长度,那么空白区域就可以绘制100的长度,但是你看不见空白,所以我们只会感觉到绘制了一条length-100的路径。
如果你按照我们的思路去做实验,那么很快你就会想到,把这个偏移量也设置成length,那么第一次的实线区间将完全得不到绘制,而直接进入空白区间,而我们的空白区间总长度也是length,因此它占用了全部的绘制区间,所以此时什么也看不到。如果空白区间小于length的话,是可以看到一点实线的(因为空白区间完了紧接着就是实线了)。
所以,我们可以设置一个百分比,取名叫phase,phase的增长是从0 .0-1.0,如果我们利用属性动画来改变它,然后根据它动态的构造一个这样的DashPathEffect:
new DashPathEffect(new float[] { length, length },
length - phase * length);
或者:
return new DashPathEffect(new float[] { phase * pathLength, pathLength },
0);
这样就能产生跟踪绘制的效果。
获取path的长度刚刚我们多次提到了path的总长度length,那么对于一条不规则的曲线来讲,要得到其长度是很难的。幸运的是,有相应的api:
// Measure the path
PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();
这样就可以把一个path从头到尾的慢慢绘制出来。
注意:上面那种方式实现动画只是针对paint的style为STROKE效果比较好,当paint的style为FILL或者是STROKE_AND_FILL时,一般不会有人需要这种效果。
2.2 效果2实现原理
找了好多方法终于把第一个动画效果实现了,那么问题了,第二个效果怎么实现?
首先需要知道的是,现在的填充效果是通过设置paint的style为FILL绘制封闭path实现的。所以,我首先想到的是可不可以根据曲线当前绘制宽度来获取封闭path图案,然而当前曲线最右边的一个点的坐标无法获得。然后想到是不是可以动态填充一个封闭path图案,上网找了一下没找到,……
终于,终于,终于,我想到了一个实现方法,还记得前面的提到的歌词变色吗?
Android 仿应用宝下载进度条
实现原理是, canvas.clipRect()方法从左向右动态截取矩形,矩形最右边就是当前曲线绘制的左右端,让后再绘制这块区域内的path,这样就实现了效果2,原来 就是这么简单。当然,如果你有其他的思路,请告诉我。
2.3 原理总结
上面说到的两种动画方法实现原理可以应用与不同使用场景。第一个原理主要可以把一条path从无到有的勾勒出来。第二个原理适用场景是按某一个方向对图形进行填充。
打个比方说,第一种像藤蔓慢慢向前曲伸,第二种像油漆刷,所到之处皆变色。(看看不说话)
3.实现
3.1定义相关成员变量
/**
* 路径总长度
*/
private float pathLength;
/**
* 动画结束标志
*/
private boolean isAnimateEnd = false;
/**
* 变化因子
*/
private float phase;
3.2代码实现曲线动态绘制
- 在drawLines方法中添加:
PathMeasure measure = new PathMeasure(path, false);
pathLength = measure.getLength();//测量path的总长度
- 同样在drawCubicPath添加:
PathMeasure measure = new PathMeasure(path, false);
pathLength = measure.getLength();//测量path的总长度
- 为了使用属性动画改变变化因子,定义以下方法:
//showWithAnimation动画开启后会调用该方法
public void setPhase(float phase) {
linePaint.setPathEffect(createPathEffect(pathLength, phase, 0.0f));
invalidate();
}
//创建DashPathEffect
private PathEffect createPathEffect(float pathLength, float phase, float offset) {
return new DashPathEffect(new float[] { phase * pathLength, pathLength }, 0);
}
- 带动画的绘制,调用该方法,实现动画绘制。
/**
* 带动画的绘制
* @param duration
*/
public void showWithAnimation(int duration){
isAnimateEnd = false;
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "phase", 0.0f, 1.0f);
animator.setDuration(duration);
animator.start();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
phase = (float) animation.getAnimatedValue();
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimateEnd = true;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
- 不带动画的绘制,内部调用动画绘制,duration设置为0。
public void show(){
showWithAnimation(0);
}
3.3代码实现path填充
private void drawFillArea(Canvas canvas) {
//继续使用前面的 path
if(line != null && line.getValues().size() > 1){
List<PointValue> values = line.getValues();
PointValue firstPoint = values.get(0);
float firstX = firstPoint.getOriginX();
PointValue lastPoint = values.get(values.size() - 1);
float lastX = lastPoint.getOriginX();
path.lineTo(lastX, axisX.getStartY());
path.lineTo(firstX, axisX.getStartY());
path.close();
linePaint.setStyle(Paint.Style.FILL);
if(line.getFillColr() == 0)
linePaint.setAlpha(100);
else
linePaint.setColor(line.getFillColr());
//根据phase计算当前绘制最右端位置
canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(firstX, 0, phase * (lastX - firstX) + firstX, getMeasuredHeight());
canvas.drawPath(path, linePaint);
canvas.restore();
path.reset();
}
}
注意:
phase是在动画的监听接口里面动态设置的,原因是这样可以使曲线和填充效果更好的同步
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
phase = (float) animation.getAnimatedValue();
}
});
4.使用
使用上改动的地方除了多添加了几个自定义属性外,唯一变化的是现在通过showWithAnimation(duration)或show()方法开启绘制。(具体使用请看demo)