最近复习了一下自定义View相关知识。看到Paint有个api,public void getTextPath(String text, int start, int end,float x, float y, Path path) ,可以获取到文字的 Path。
文字的绘制,虽然是使用 Canvas.drawText() 方法,但其实在下层,文字信息全是被转化成图形,对图形进行绘制的。
getTextPath() 方法,获取的就是目标文字所对应的 Path 。这个就是所谓「文字的 Path」。
那么我通过ValueAnimator控制这个路径的绘制进度,不是可以显示文本的绘制过程了吗。看下效果图
如果对PathMeasure不够熟悉的朋友,可以先去看下PathMeasure的使用。
整个流程分下面几步:
一、init():
获取待绘制文本的Path,初始化对应的PathMeasure, 迭代Path的每个轮廓,计算Path的总长度
二、onDraw
- 根据动画的进度,计算待绘制的路径长度needDrawLength
- 判断当前轮廓的总长度是否大于要绘制的长度,如果大于,则当前轮廓只用绘制部分,结束循环;如果小于,则当前轮廓完整绘制,并和已绘制轮廓总长度累加,循环当前步骤
下面贴完整代码
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class PathMeasureView extends View {
private static final String TAG = "PathMeasureView";
public PathMeasureView(Context context) {
this(context,null);
}
public PathMeasureView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public PathMeasureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private Paint mPaint;
private Path mPath; //整个文本所对应的Path
private PathMeasure mPathMeasure; //mPath对应的PathMeasure,用来控制mPath需要绘制的比例
private Path dstPath; //每次重绘时,需要绘制的path
private String drawText = "睁眼看世界"; //待绘制文本
private ValueAnimator valueAnimator;
private float mProgress; //当前进度
private float mTotalLength; //要绘制的文本所对应Path的总长度
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize(200);
mPath = new Path();
dstPath = new Path();
//获取待绘制文本的Path
mPaint.getTextPath(drawText,0,drawText.length(),50,200,mPath);
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mPath,false);
//计算总长度
mTotalLength = mPathMeasure.getLength();
while (mPathMeasure.nextContour()){
mTotalLength += mPathMeasure.getLength();
}
mPathMeasure.setPath(mPath,false);
//执行动画
valueAnimator = ValueAnimator.ofFloat(0,1);
valueAnimator.setDuration(20000);
valueAnimator.setRepeatMode(ValueAnimator.RESTART);
valueAnimator.setRepeatCount(-1);
valueAnimator.addUpdateListener(animation -> {
mProgress = (float) animation.getAnimatedValue();
invalidate();
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//因为上次绘制时,将PathMeasure循环到了中间的某个轮廓。所以每次开始绘制,一定要对mPathMeasure重新设置Path,回到第1个轮廓
mPathMeasure.setPath(mPath,false);
float needDrawLength = mTotalLength * mProgress;
float consumedLength = mPathMeasure.getLength(); //当前path的总长度
while (consumedLength<needDrawLength){
//当前轮廓的总长度小于要绘制的总长度,所以当前轮廓需要完整绘制
mPathMeasure.getSegment(0,mPathMeasure.getLength(),dstPath,true);
canvas.drawPath(dstPath,mPaint);
mPathMeasure.nextContour();
consumedLength += mPathMeasure.getLength();
}
//加上最后一段轮廓的长度后,超过了要绘制的总长度,所以先减去最后一个轮廓的长度,然后对最后一段轮廓部分绘制
consumedLength -= mPathMeasure.getLength();
mPathMeasure.getSegment(0,needDrawLength - consumedLength,dstPath,true);
canvas.drawPath(dstPath,mPaint);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
valueAnimator.start();
}
}