前言
根据Gcssloop所学习的自定义View整理与笔记。
一. 简单了解PathMeasure
** PathMeasure是一个用来测量Path的类**
1. 相关方法
- 构造方法
方法名 | 作用 |
---|---|
PathMeasure() | 创建一个空的PathMeasure |
PathMeasure(Path path, boolean forceClosed) | 创建PathMeasure并关联一个指定的Path(Path)需要已经创建完成 |
- 公共方法
返回值 | 方法名 | 作用 |
---|---|---|
void | setPath(Path path, boolean forceClosed) | 关联一个path |
boolean | isClosed() | 是否关闭 |
float | getLength() | 获取path的长度 |
boolean | nextContour() | 跳转到下一个轮廓 |
boolean | getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) | 截取片段 |
boolean | getPosTan(float distance,floast[] pos,float[] tan) | 获取指定长度的位置坐标及该点切线值 |
boolean | getMatrix(float distance, Matrix matrix, int flags) | 获取指定长度的位置坐标及该点Matrix |
二.详细介绍各个方法
1.构造函数
- PathMeasure()
用这个构造函数可创建一个空的 PathMeasure,但是使用PathMeasure对象之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。
- PathMeasure (Path path, boolean forceClosed)
- path:被关联的path,其实和创建一个空的 PathMeasure再调用 setPath 方法来与 Path 进行关联效果是一样的
- forceClosed:用来确保path闭合,如果设置为true,则不论之前path是否闭合,都会自动闭合该path(如果path可以闭合的话),但是不会影响之前path的状态。
注意:forceClosed 的设置状态可能会影响测量结果,如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。
demo:
paint.setStyle(Paint.Style.STROKE);
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.d("PathMeasure", "forceClosed=false---->" + measure1.getLength());//600
Log.d("PathMeasure", "forcedClosed=true----->" + measure2.getLength());//800
canvas.drawPath(path, paint);
2.setPath、 isClosed 和 getLength
- setPath: 和构造函数中的path作用是一样的,就是设置关联的path
- isClosed: 用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为true。
- getLength:用于获取path的长度
3.boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
- 返回值 boolean:判断截取是否成功,true 表示截取成功,结果添加到dst中,false 截取失败,不会改变dst中内容
- startD:开始截取位置距离Path起点的长度,取值范围:0<=startD
- stopD:结束截取位置距离Path起点的长度,取值范围:0<=startD
- dst: 截取的Path将会添加到dst中,注意:是添加,不是替换
- startWithMoveTo:起始点是否使用moveTo,用于保证截取的Path第一个点位置不变,如果不使用,则会改变截取的path的起始点。
如果不清楚的话,木关系,下边会有demo的哦 - stopD:结束截取位置距离Path起点的长度,取值范围:0<=startD
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
canvas.translate(400, 400);
Path path = new Path();
//创建一个顺时针的矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
Path dst = new Path();
dst.lineTo(-300, -300);
// 截取矩形的一部分,添加到dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200, 600, dst, true);
canvas.drawPath(dst, paint);
//仅仅将上边的demo的startWithMoveTo设为false
measure.getSegment(200, 600, dst, false);
如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)
4. nextContour
nextContour 用于跳转到下一条曲线,true表示成功,false表示失败。
例如下边的图,便是内外两条曲线:
举个栗子:
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
canvas.translate(400, 400);
Path path = new Path();
// 添加小矩形
path.addRect(-100, -100, 100, 100, Path.Direction.CCW);
// 添加大矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
// 绘制 Path
canvas.drawPath(path, paint);
// 将Path与PathMeasure关联
PathMeasure measure = new PathMeasure(path, true);
// 获得第一条路径的长度
float len1 = measure.getLength();
// 跳转到下一条路径
measure.nextContour();
// 获得第二条路径的长度
float len2 = measure.getLength();
// 输出两条路径的长度
Log.i("LEN", "len1=" + len1); //结果 800
Log.i("LEN", "len2=" + len2); //结果 1600
1.曲线的顺序与 Path 中添加的顺序有关。
2.getLength 获取到到是当前一条曲线分长度,而不是整个 Path 的长度。
3.getLength 等方法是针对当前的曲线
5.boolean getPosTan (float distance, float[] pos, float[] tan)
用于得到路径上某一长度的位置以及该位置的正切值。
- 返回值boolean:判断获取是否成功,true表示成功,数据会存入pos和tan中;false表示失败,pos和tan不变
- distance: 距离path起点的长度,取值范围 0 <= distance <= getLength
- pos:该点的坐标值,当前点在画布上的位置,x、y坐标
- tan:该点的正切值,当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。tan[0]是邻边边长,tan[1]是对边边长,tanΘ=对边/ 邻边=tan[1]/tan[0]。
例如,我们需要计算旋转角度,则可以这样
//tan[1],tan[0]千万千万不要反了
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
举个栗子吧,注意箭头的方向哦O(∩_∩)O~
private void init() {
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
matrix = new Matrix();
pos = new float[2];
tan = new float[2];
BitmapFactory.Options options = new BitmapFactory.Options();
//缩放图片
options.inSampleSize = 2;
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.row, options);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//路径
Path path = new Path();
path.lineTo(200, 200);
path.lineTo(400, 200);
path.lineTo(300, 500);
path.lineTo(0, 700);
//进度
currentValue += 0.05;
if (currentValue > 1) {
currentValue = 0;
}
PathMeasure pathMeasure = new PathMeasure(path, false);
int length = (int) (pathMeasure.getLength() * currentValue);
pathMeasure.getPosTan(length, pos, tan);
//获取旋转角度
float degree = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);
//设置图片旋转角度和偏移量,matrix的方法可以类比canvas的操作方法
matrix.reset();
matrix.postRotate(degree, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
matrix.postTranslate(pos[0] - bitmap.getWidth() / 2, pos[1] - bitmap.getHeight() / 2);
canvas.drawPath(path, paint);
canvas.drawBitmap(bitmap, matrix, paint);
try {
Thread.sleep(300);
}
catch (InterruptedException e) {
e.printStackTrace();
}
//最好使用 线程 或者 ValueAnimator 来控制界面的刷新
invalidate();
}
大家也可以试试这样子让箭头根据圆来旋转移动
6. boolean getMatrix (float distance, Matrix matrix, int flags)
用于得到路径上某一长度的位置以及该位置的正切值的矩阵
- 返回值boolean: 判断获取是否成功 ,true成功,数据会存入matrix中,false失败,matrix内容不变
- distance: 距离path起点的长度,取值范围0<=distance<=getLength
- matrix: 根据flags封装好的matrix,根据flags的设置而存入不同的内容
- flags: 规定哪些内容会存入到matrix中,可选择POSITION_MATRIX_FLAG(位置) 、ANGENT_MATRIX_FLAG(正切),如果两个选项都想选择,可以将两个选项之间用 | 连接起来
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//路径
Path path = new Path();
path.lineTo(200, 200);
path.lineTo(400, 200);
path.lineTo(300, 500);
path.lineTo(0, 700);
//进度
currentValue += 0.05;
if (currentValue > 1) {
currentValue = 0;
}
PathMeasure pathMeasure = new PathMeasure(path, false);
float length =pathMeasure.getLength() * currentValue;
//---------改变下边的内容------------------------------
pathMeasure.getMatrix(length,matrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
// 将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
matrix.preTranslate(-bitmap.getWidth()/2,-bitmap.getHeight()/2);
//---------改变的内容结束------------------------------
canvas.drawPath(path, paint);
canvas.drawBitmap(bitmap, matrix, paint);
try {
Thread.sleep(300);
}
catch (InterruptedException e) {
e.printStackTrace();
}
invalidate();
}
效果图和上边一样哦
1.对 matrix 的操作必须要在 getMatrix 之后进行,否则会被 getMatrix 重置而导致无效。
2.矩阵对旋转角度默认为图片的左上角,我们此处需要使用 preTranslate 调整为图片中心。
3.使用pre,越靠后越先执行,即后调用的pre操作先执行。会在后续Matrix章节详细讲解
三. postInvalidate()和Invalidate()区别
这两个函数的作用都是用来重绘控件的,但区别是Invalidate()一定要在UI线程执行,如果不是在UI线程就会报错。而postInvalidate()则没有那么多讲究,它可以在任何线程中执行,而不必一定要是主线程。其实在postInvalidate()就是利用handler给主线程发送刷新界面的消息来实现的,所以它是可以在任何线程中执行,而不会出错。而正是因为它是通过发消息来实现的,所以它的界面刷新可能没有直接调Invalidate()刷的那么快。
所以在我们确定当前线程是主线程的情况下,还是以invalide()函数为主。当我们不确定当前要刷新页面的位置所处的线程是不是主线程的时候,还是用postInvalidate为好;
四.一个动画撒
代码如下:
package com.xiaohongchun.redlips.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import com.orhanobut.Logger;
import com.xiaohongchun.redlips.R;
import com.xiaohongchun.redlips.record.Util;
public class SuccessIcon extends View {
private Paint paint;
private Path path, dst;
private PathMeasure pathMeasure;
private float value;
private float radius;
private int size;
private ValueAnimator valueAnimator;
public SuccessIcon(Context context) {
this(context, null);
}
public SuccessIcon(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public void startAnimater() {
Logger.d("pngpng","size="+size);
size = Math.min(getMeasuredWidth(), getMeasuredHeight());
radius = size / 2 - Util.dipToPX(getContext(), 2);
initPathPaint();
initAnimater();
}
private void initPathPaint() {
paint = new Paint();
paint.setColor(Color.RED);
paint.setAntiAlias(true);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(Util.dipToPX(getContext(), 1));
paint.setStyle(Paint.Style.STROKE);
path = new Path();
dst = new Path();
float start = (float) -Math.cos(Math.toRadians(45)) * radius / 2;//开始点
path.moveTo(start, start);
path.lineTo(0, 0);
float end = (float) Math.cos(Math.toRadians(45)) * radius;//线的结束点
path.rLineTo(end, -end);
path.arcTo(new RectF(-1 * radius, -1 * radius, radius, radius), -45, -350);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, false);
}
private void initAnimater() {
if (valueAnimator == null) {
valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.setDuration(600);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
value = (float) animation.getAnimatedValue();
postInvalidate();
}
});
valueAnimator.start();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (size > 0) {
canvas.translate(size / 2, size / 2);
pathMeasure.getSegment(0, pathMeasure.getLength() * value, dst, true);
dst.rLineTo(0, 0);//如果没有关闭硬件加速,则加上这句话,关闭了,则不需要
canvas.drawPath(dst, paint);
}
}
}
icon = (SuccessIcon) findViewById(R.id.success_icon);
icon.post(new Runnable() {
@Override
public void run() {
icon.startAnimater();
}
});
后记
这个是 http://www.gcssloop.com/customview/Path_PathMeasure 所实现的动画,
源码是 https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/Code/SearchView.java
主要利用了getSegment进行绘制,这里就不讲解了
参考网站
自定义控件三部曲之绘图篇