版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、PathMeasure 基础
从名字就可以看出, PathMeasure 是一个用来测量 Path 的类。
1.构造方法 PathMeasure()
创建一个空的 PathMeasure。
2.构造方法 PathMeasure(Path path, boolean forceClosed)
创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)。
这边需要强调的是参数 forceClosed : 当 Path 有调用 close() 方法的时候,forceClosed 为 true 或 false 没有区别。当 Path 没有调用 close() 方法的,且 forceClosed 为 true 的时候,调用 PathMeasure 的 getLength() 方法,长度会增加关闭区域的长度。
有调用 close() 方法:
Path path = new Path();
path.lineTo(0,200);
path.lineTo(200,200);
path.lineTo(200,0);
path.close();
PathMeasure measure1 = new PathMeasure(path,false);
PathMeasure measure2 = new PathMeasure(path,true);
Log.e(TAG, "forceClosed=false length = "+measure1.getLength());
Log.e(TAG, "forceClosed=true length = "+measure2.getLength());
canvas.drawPath(path,mDeafultPaint);
效果:
日志输出:
E/MyView: forceClosed=false length = 800.0
E/MyView: forceClosed=true length = 800.0
没有调用 close() 方法:
Path path = new Path();
path.lineTo(0,200);
path.lineTo(200,200);
path.lineTo(200,0);
PathMeasure measure1 = new PathMeasure(path,false);
PathMeasure measure2 = new PathMeasure(path,true);
Log.e(TAG, "forceClosed=false length = "+measure1.getLength());
Log.e(TAG, "forceClosed=true length = "+measure2.getLength());
canvas.drawPath(path,mDeafultPaint);
效果:
日志输出:
E/MyView: forceClosed=false length = 600.0
E/MyView: forceClosed=true length = 800.0
3.getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
截取片段。
startD : 截取片段的起始长度(取值范围在 0 ~ Path 的长度之间)
stopD : 截取片段的终止长度(取值范围在 0 ~ Path 的长度之间),切必须大于 startD,否则取不到
dst : 截取后片段会赋值给 dst
startWithMoveTo : 起始点是否使用 moveTo 方法(一般情况为 true)
如果 startWithMoveTo 设置为 false的话,dst 中保存的 Path 是就会不断被添加的,而不是被覆盖,新增的片段会从上一次 Path 终点开始计算,这样所有截取的 Path 就会连续起来。
这边比较难理解的是第四个参数 startWithMoveTo。为了方便看效果,这里先建立了一个坐标系,并把中心移到屏幕中心。
startWithMoveTo 为 true:
// 平移坐标系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 画坐标线
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path();
// 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();
// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200, 600, dst, true);
canvas.drawPath(path,mPaint);
canvas.drawPath(dst, mDeafultPaint);
效果:
为 dst 添加一条初始的线段。
// 平移坐标系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 画坐标线
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path();
// 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(100, 100);
// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200, 600, dst, true);
canvas.drawPath(path,mPaint);
// 绘制 dst
canvas.drawPath(dst, mDeafultPaint);
效果:
startWithMoveTo 为 false:
// 平移坐标系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 画坐标线
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path();
// 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();
// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
// 截取一部分存入dst中,不使用 moveTo
measure.getSegment(200, 600, dst, false);
canvas.drawPath(path,mPaint);
canvas.drawPath(dst, mDeafultPaint);
效果:
startWithMoveTo 为 false,截取的 Path 没有被 moveTo 到起始位置,所以默认 moveTo 到(0, 0)所以导致了绘制的红线从原点开始
为 dst 添加一条初始的线段。
// 平移坐标系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 画坐标线
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path();
// 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(100, 100);
// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200, 600, dst, false);
canvas.drawPath(path,mPaint);
// 绘制 dst
canvas.drawPath(dst, mDeafultPaint);
效果:
4.nextContour()
获取下一条曲线轮廓,当 Path 里面有多条轮廓的时候,会依次取获取。
// 平移坐标系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 画坐标线
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path();
Path path1 = new Path();
Path path2 = new Path();
// 添加小矩形
path1.addRect(-100, -100, 100, 100, Path.Direction.CW);
// 添加大矩形
path2.addRect(-200, -200, 200, 200, Path.Direction.CW);
path.op(path1,path2, Path.Op.XOR);
canvas.drawPath(path,mDeafultPaint);
PathMeasure measure = new PathMeasure(path, false);
float len1 = measure.getLength();
// 跳转到下一条路径
measure.nextContour();
float len2 = measure.getLength();
Log.d(TAG,"len1 = "+len1);
Log.d(TAG,"len2 = "+len2);
注:这里必须采用自己设置线条的叠加模式,否则会被覆盖。
效果:
日志输出:
D/MyView: len1 = 1600.0
D/MyView: len2 = 800.0
5.getPosTan(float distance, float[] pos, float[] tan)
获取指定长度的位置坐标及该点切线值 tangle。
distance: 位置距起始点的距离
pos: 指定位置的坐标,长度为2 (x==pos[0], y==pos[1])
tan: 这个值长度也是2,比较复杂,还是直接看后面效果
效果:
public class MyView1 extends View {
private float currentValue = 0; // 用于纪录当前的位置,取值范围[0,1]映射Path的整个长度
private float[] pos; // 当前点的实际位置
private float[] tan; // 当前点的tangent值,用于计算图片所需旋转的角度
private Bitmap mBitmap; // 箭头图片
private Matrix mMatrix; // 矩阵,用于对图片进行一些操作
private Paint mDeafultPaint;
private int mViewWidth;
private int mViewHeight;
private Paint mPaint;
public MyView1(Context context) {
super(context);
init(context);
}
private void init(Context context) {
pos = new float[2];
tan = new float[2];
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8; // 缩放图片
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
mMatrix = new Matrix();
mDeafultPaint = new Paint();
mDeafultPaint.setColor(Color.RED);
mDeafultPaint.setStrokeWidth(5);
mDeafultPaint.setStyle(Paint.Style.STROKE);
mPaint = new Paint();
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
// 平移坐标系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 画坐标线
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path(); // 创建 Path
path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
if (currentValue >= 1) {
currentValue = 0;
}
// 获取当前位置的坐标以及趋势
measure.getPosTan(measure.getLength() * currentValue, pos, tan);
// 重置Matrix
mMatrix.reset();
// 计算图片旋转角度
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
// 旋转图片
mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
// 将图片绘制中心调整到与当前点重合
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);
canvas.drawPath(path, mDeafultPaint);
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);
invalidate();
}
}
核心代码:
// 获取当前位置的坐标以及趋势
measure.getPosTan(measure.getLength() * currentValue, pos, tan);
// 重置Matrix
mMatrix.reset();
// 计算图片旋转角度
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
// 旋转图片
mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
// 将图片绘制中心调整到与当前点重合
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);
对与移动图片不仅进行了平移,也进行了旋转。平移这边是采用后乘平移 postTranslate,这个平移是相对于坐标点(0, 0)。这边比较难懂的是旋转的角度,
以(0, 0)为圆心,新建半径为 1 的圆。直线旋转后与圆相交的点的 x, y 坐标即 getPosTan 方法中 tan 的两个值。
利用反 tan 函数,可以登出旋转角度为:Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI。
6.getMatrix(float distance, Matrix matrix, int flags)
获取指定长度的位置坐标及该点 Matrix (矩阵)
distance: 位置距起始点的距离
matrix: 指定位置的矩阵
flags : 标志,flags 有两个选项值,表示矩阵关联的关系
public static final int POSITION_MATRIX_FLAG = 0x01; // must match flags in SkPathMeasure.h //位置信息
public static final int TANGENT_MATRIX_FLAG = 0x02; // must match flags in SkPathMeasure.h //切边信息
核心代码:
// 获取当前位置的坐标以及趋势的矩阵
measure.getMatrix(measure.getLength() * currentValue, mMatrix,
PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
// 将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
把 getPosTan 的核心代码替换成这个也能实现一样的功能。
注:这边采用平移前乘矩阵,且由于 getMatrix 标志用到了位置信息,所以平移的时候相对位置点是路径上选中的那个点。
三、getSegment 实现的进度圆圈
代码比较简单,就不再具体注释了。
public class LoadingView1 extends View {
private Path mPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private float mAnimatorValue;
private Path mDst;
private float mLength;
public LoadingView1(Context context) {
this(context,null);
}
public LoadingView1(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public LoadingView1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
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();
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
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;
mPathMeasure.getSegment(0, stop, mDst, true);*/
float stop = mLength * mAnimatorValue;
float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength));
mPathMeasure.getSegment(start, stop, mDst, true);
canvas.drawPath(mDst, mPaint);
}
}
四、水波纹小船改变方向
对上一节的小船效果进行优化,是小船的船头随波浪上下起伏。
主要是在绘制小船的时候,运用 getPosTan 或者 getMatrix 对小船进行旋转。
getPosTan 旋转
mPathMeasure = new PathMeasure(path, false);
//方案一,getPosTan
mPathMeasure.getPosTan(width / 2 + length * (1 - faction), pos, tan);
float degrees = (float) (Math.atan2(tan[1], tan[0])*180f / Math.PI);
mMatrix.postRotate(degrees, mBitmap.getWidth()/2, mBitmap.getHeight() / 2);
mMatrix.postTranslate(pos[0]- mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight());
canvas.drawBitmap(mBitmap, mMatrix, paint);
getMatrix 旋转
mPathMeasure = new PathMeasure(path, false);
mPathMeasure.getMatrix(width / 2 + length * (1 - faction), mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
mMatrix.preTranslate(- mBitmap.getWidth() / 2, - mBitmap.getHeight());
canvas.drawBitmap(mBitmap, mMatrix, paint);