所阅读博客地址:PathMeasure之迷径追踪,下文简称为《迷径》
另外参考博客:安卓自定义View进阶-PathMeasure、使用DashPathEffect绘制一条动画曲线
1、
在《迷径》中的第一个例子,是实现下图的效果:
开始对其中的部分代码不太理解,后来想了一会儿之后想通了,以此记录:
public class PathPainter extends View {
...
public PathPainter(Context context, AttributeSet attrs) {
super(context, attrs);
mPathMeasure = new PathMeasure();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPath = new Path();
mPath.addCircle(400, 400, 100, Path.Direction.CW);
mPathMeasure.setPath(mPath, true);
//获取路径的长度,这里即圆的周长
mLength = mPathMeasure.getLength();
mDst = new Path();
//ofLoat() 是从 0 - 1,用来进度
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//例如,当 mAnimatorValue 值为 0.1f 时,就表示整个圆周长的 10% 位置处(相对于圆的起点位置)
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(2000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDst.reset();
// 硬件加速的BUG
mDst.lineTo(0,0);
float stop = mLength * mAnimatorValue;
//虽然被截取的 Path 片段是被添加到 dst 中,而不是每次被覆盖,
// 但是由于在本次Demo中总是不断截取同一个圆的片段(且片段的长度越来越大直到整个为整个圆的路径),
// 因此每次添加的片段路径是周期性的,且在前面利用 reset() 清空了上一次的数据,跟覆盖的效果一样
mPathMeasure.getSegment(0, stop, mDst, true);
canvas.drawPath(mDst, mPaint);
}
}
2、
然后是实现下面的动画
就只需要将原来的绘制的起点改为如下
float stop = mLength * mAnimatorValue;
float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength));
mPathMeasure.getSegment(start, stop, mDst, true);
其中的关键点就是那个公式,简化之后为:
mLength * (mAnimatorValue - 0.5 + |mAnimatorValue - 0.5|)
当 mAnimatorValue <= 0.5 时, 实际上 start = 0;
当 mAnimatorValue > 0.5 时, start 为 mLength * (2 * mAnimatorValue -1) ,范围为 mLength * (0 , 1] ;
所以在半圈之前,效果跟第一个动图的效果是一样的,但是超过半圈之后,绘制的路径的起点会快速向终点靠近,以致于所绘制的路径越来越短,最后起始点重合。
3、DashPathEffect
DashPathEffect作用是将Path的线段虚线化。
DashPathEffect (float[] intervals, float phase)
对于上面的两个参数,第一个是间隔数组,且数组长度需要 >= 2,长度最后为偶数的,否则最后一个奇数位的会失效。****第二个参数表示偏移长度。接下来就用图片来具体解释一下。
以 new DashPathEffect(new float[]{150,150}, 0);
为例,其中最左边的第一根辅助线的横坐标为第 100,最后一根辅助线的横坐标为第 850(每隔 50 一根辅助线),对应的线段也是从 100 画到 850,在使用了 DashPathEffect
之后就是如下结果:
而当把 phase
设置为 50 的时候,即 new DashPathEffect(new float[]{150,150}, 50);
,其余条件不变,结果就变成如下:
而当把 phase
设置为 100 的时候,,结果就变成如下:
然后按照下面的想法想,可能会跟实际的不同,但是按照下面的那样想会容易一点,便于理解第4 节效果图的实现原理:
根据上面的那些图,就可以很直观的展示出参数 phase
的作用,即在从起点画了长度为 new float[]{X,Y}
中 X 的长后,往左偏移回 phase
值的长度,然后开始绘制 Y 长度的空白,再绘制 X 长度的线段,再绘制 Y 长度的空白,以及往复,直到到达终点,之后就不会再绘制了,即使此时空白段还没有开始绘制或者还没有绘制完,一定要记住这里,不然在第 4 节那儿可能会被坑,以为空白线段一定要绘制设定的长度才算。
这这个例子中,先是从第一根辅助线开始到第四根辅助线画出一条线段,即 150 长的线段,但是因为 phase = 100
,所以要偏移回(即回退) 100 的长度,即从第四根辅助线退回到第二根,在回退的过程中会抹去已经绘制的线段(用形象的说法就是用橡皮擦擦去),然后从第二根辅助线开始画长度 150 长的空白,再画 150 的线段,如此往复下去。(这样想,下面那一节展示的那个效果图就能想清楚为什么会是那样的效果了)
这里再附上 new DashPathEffect(new float[]{150,150}, 200);
的结果图,其余条件按上述所示:
这里就比较特殊了,但本质思想还是没变,先是从第一个辅助线(还是最左边第一根红色的)开始到第四根辅助线画出一条线段,即 150 长的线段,但是因为 phase = 200
,所以要偏移回(即回退) 200 的长度,即使回退到了起点也不会停止,直到回退到那条蓝色辅助线(为了体现效果额外加的)那儿为止,然后从该线开始画长度 150 长的空白,再画 150 的线段,如此往复下去。
注意,当 phase
为负数时,则需要变换一下思想。
图 1
图 2
即虽然实际上线段整体是从左往右画的(即从 100 画到 850),当 phase
为负数时,需要想作是从右画到左(即从 850 到 100),然后以上面图 2 为例,先是从第十六根(即最右边到)红色辅助线开始到第十三根辅助线画出一条线段,即 150 长的线段,但是因为 phase = -200
,所以要偏移回(即回退) 200 的长度,即从第十三根辅助线退回到最右边的蓝色辅助线(即偏移回 200),然后从蓝色辅助线开始向左画长度 150 长的空白,再画 150 的线段,如此往复下去,直到第一根辅助线。
4、
然后《迷径》中如下的效果图:
首先需要明确的是,三角的画的方向本身就是从左上角那个点开始,然后往下,再拐角往右上,再回到起点。
然后就是利用 ValueAnimator.ofFloat(1, 0)
,加上下面的核心代码:
fraction = (float) valueAnimator.getAnimatedValue();
mEffect = new DashPathEffect(new float[]{length, length}, fraction * length);
mPaint.setPathEffect(mEffect);
之后不断重绘就行了。
假设三角形周长为600,则 phase
的值就是从 600 减到 0 的,然后利用第 3 点的解释说明去在脑中模拟或者自己用比试试,就能明白这个效果图的原理了。
或者看下面我自己录制的 GIF 图(请忽略做工),其中黑色线段代表实际绘制的线段,白色线段代表回退的效果(假设回退了500,即 phase = fraction * length = 500
),紫色线段代表实际空白的绘制。
或者是
ValueAnimator.ofFloat(0, 1)
+ mEffect = new DashPathEffect(new float[]{length, length}, length - fraction * length);