Android 项目:画图白板APP开发(四)——笔锋(单 Path)
上一章讲解了如何通过多个 Path 叠加形成笔锋效果,还有另外的方式实现笔锋,并且只需要一条Path就可以了。在讲解具体方案之前,我们需要了解一个有意思的工具 PathMeasure ,这是一个非常强大且实用的工具,常用于高级动画和路径绘制。
一、PathMeasure 介绍
它的主要作用是对一个已有的 Path 对象进行测量,从而获取该路径的详细信息,例如:
路径的总长度。
路径上任意位置(从起点开始的距离)的坐标点 (x, y) 和切线角度 (tangent)。
截取原始路径的某一段,生成一个新的 Path 片段。
正因为能获取到路径上每个点的精确位置和方向,它成为了实现各类轨迹动画(如飞机沿航线飞行、箭头沿曲线移动)的核心组件。网上的很多案例,可以搜索看看,我在这里对其中的方法简单介绍下。
(1)构造方法
方法名 | 释义 |
---|---|
PathMeasure() | 创建一个空的PathMeasure |
PathMeasure(Path path, boolean forceClosed) | 创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)。 |
- 无参构造函数:可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的。如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。
- 有参构造函数: forceClosed:用来确保 Path 闭合,如果设置为 true, 则不论之前Path是否闭合,都会自动闭合该 Path(如果Path可以闭合的话);
(2)setPath方法
void setPath(Path path, boolean forceClosed)
作用:此方法是 PathMeasure 与 Path 关联的重要方法,效果和构造函数中两个参数的作用是一样的。
(3)isClosed方法
boolean isClosed()
作用:此方法用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为true。
(4)getLength方法
float getLength()
作用:此方法用于获取 Path 路径的总长度。
(5)nextContour方法
boolean nextContour()
作用: Path 可以由多条曲线构成,但不论是 getLength 方法, 还是getgetSegment 或者其它方法,都只会在其中第一条线段上运行。此 nextContour方法 就是用于跳转到下一条曲线到方法。如果跳转成功,则返回 true, 如果跳转失败,则返回 false。
(6)getSegment方法
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
- 返回值boolean:判断截取是否成功(true 表示截取成功,结果存入dst中,false 截取失败,不会改变dst中内容);
- float startD:开始截取位置距离 Path 起点的长度(取值范围:
0 <= startD < stopD <= Path总长度
);- float stopD:结束截取位置距离 Path 起点的长度(取值范围:
0 <= startD < stopD <= Path总长度
);- Path dst:截取的 Path 将会添加到 dst 中(注意: 是添加,而不是替换);
- boolean startWithMoveTo:起始点是否使用 moveTo,用于保证截取的 Path 第一个点位置不变(true表示保证截取得到的 Path 片段不会发生形变,false表示保证存储截取片段的 Path(dst) 的连续性);
作用:用于获取Path路径的一个片段。(如果 startD、stopD 的数值不在取值范围 [0, getLength]
内,或者 startD == stopD
则返回值为 false,不会改变 dst 内容)。
(7)getPosTan方法
boolean getPosTan(float distance, float[] pos, float[] tan)
- 返回值(boolean):判断获取是否成功(true表示成功,数据会存入 pos 和 tan 中,false 表示失败,pos 和 tan 不会改变);
- float distance:距离 Path 起点的长度 取值范围:
0 <= distance <= getLength
;
- float[] pos:该点的坐标值,坐标值:
(x==[0], y==[1])
;
- float[] tan:该点的正切值,正切值:
(x==[0], y==[1])
;
作用:用于获取路径上某点的坐标以及该位置的正切值,即切线的坐标。相当于是getPos
、getTan
两个API的集合。
(8)getMatrix方法
boolean getMatrix(float distance, Matrix matrix, int flags)
- 返回值(boolean):判断获取是否成功(true表示成功,数据会存入matrix中,false 失败,matrix内容不会改变);
- float distance:距离 Path 起点的长度(取值范围:
0 <= distance <= getLength
);- Matrix matrix:根据 falgs 封装好的matrix,会根据 flags 的设置而存入不同的内容;
- int flags:规定哪些内容会存入到matrix中(可选择POSITION_MATRIX_FLAG位置 、ANGENT_MATRIX_FLAG正切 );
作用:用于得到路径上某一长度的位置以及该位置的正切值的矩阵。
二、单 Path 笔锋(画点成线)
这个要使用上面的所提到的工具 PathMeasure ,接下来用一个简单的例子看看如何使用。
(1)效果图1
这是一个宽度渐变的曲线,同时通过画点成线绘制曲线
(2)代码1
public class GradientPointLineView extends View {private Paint paint;private Path path;private List<PointF> points;public GradientPointLineView(Context context) {super(context);init();}public GradientPointLineView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init();}private void init() {paint = new Paint(Paint.ANTI_ALIAS_FLAG);paint.setStyle(Paint.Style.FILL);path = new Path();points = new ArrayList<>();}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if (points.size() < 2) return;// 创建路径path.reset();path.moveTo(points.get(0).x, points.get(0).y);for (int i = 1; i < points.size(); i++) {path.lineTo(points.get(i).x, points.get(i).y);}//绘制所有的点,然后再每个点渐变// 使用 PathMeasure 计算路径上的点PathMeasure pathMeasure = new PathMeasure(path, false);//float pathLength = pathMeasure.getLength();// 设置最大和最小点的大小float maxRadius = 20f;float minRadius = 5f;// 沿路径绘制渐变大小的点float distance = 0;float step = 5f; // 点之间的间隔(像素)while (distance < pathLength) {float[] pos = new float[2];float[] tan = new float[2];pathMeasure.getPosTan(distance, pos, tan);// 计算当前点的半径(根据距离起点位置渐变)float progress = distance / pathLength;float radius = maxRadius - (maxRadius - minRadius) * progress;// 绘制圆点canvas.drawCircle(pos[0], pos[1], radius, paint);distance += step;}// 确保终点被绘制float[] endPos = new float[2];pathMeasure.getPosTan(pathLength, endPos, null);canvas.drawCircle(endPos[0], endPos[1], minRadius, paint);}public void addPoint(float x, float y) {points.add(new PointF(x, y));invalidate();}public void clearPoints() {points.clear();invalidate();}
}
- 通过PathMeasure计算整个曲线的长度,每过 5 像素就画一个点
(3)效果图2
之前的效果还可以得到提升,增加贝赛尔曲线和颜色渐变的操作。
(4)代码2
public class SmoothQuadBezierView extends View {private Paint paint;private Path path;private List<PointF> points;private float maxRadius = 20f;private float minRadius = 2f;private int startColor = Color.RED;private int endColor = Color.BLUE;public SmoothQuadBezierView(Context context) {super(context);init();}public SmoothQuadBezierView(Context context, AttributeSet attrs) {super(context, attrs);init();}private void init() {paint = new Paint(Paint.ANTI_ALIAS_FLAG);paint.setStyle(Paint.Style.FILL);path = new Path();points = new ArrayList<>();}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if (points.size() < 2) return;// 创建平滑的二次贝塞尔曲线路径path.reset();createSmoothQuadPath();// 测量路径长度PathMeasure pathMeasure = new PathMeasure(path, false);float pathLength = pathMeasure.getLength();// 沿路径绘制渐变点drawGradientPoints(canvas, pathMeasure, pathLength);// 绘制明显的起点和终点//drawEndPoints(canvas);}/*** 创建更平滑的二次贝塞尔曲线路径*/private void createSmoothQuadPath() {path.moveTo(points.get(0).x, points.get(0).y);if (points.size() == 2) {// 两个点:使用中间控制点PointF p0 = points.get(0);PointF p1 = points.get(1);float controlX = (p0.x + p1.x) / 2;float controlY = (p0.y + p1.y) / 2;path.quadTo(controlX, controlY, p1.x, p1.y);} else {// 多个点:创建连续平滑曲线for (int i = 1; i < points.size(); i++) {PointF prev = points.get(i - 1);PointF current = points.get(i);if (i == 1) {// 第一个线段float controlX = prev.x + (current.x - prev.x) * 0.5f;float controlY = prev.y + (current.y - prev.y) * 0.5f;path.quadTo(controlX, controlY, current.x, current.y);} else if (i == points.size() - 1) {// 最后一个线段PointF prevPrev = points.get(i - 2);float controlX = prev.x + (prev.x - prevPrev.x) * 0.25f;float controlY = prev.y + (prev.y - prevPrev.y) * 0.25f;path.quadTo(controlX, controlY, current.x, current.y);} else {// 中间线段:使用前后点计算更平滑的控制点PointF next = points.get(i + 1);float controlX = current.x - (next.x - prev.x) * 0.25f;float controlY = current.y - (next.y - prev.y) * 0.25f;path.quadTo(controlX, controlY, current.x, current.y);}}}}/*** 沿路径绘制渐变点*/private void drawGradientPoints(Canvas canvas, PathMeasure pathMeasure, float pathLength) {float distance = 0;float step = 3f;while (distance < pathLength) {float[] pos = new float[2];pathMeasure.getPosTan(distance, pos, null);float progress = distance / pathLength;float radius = calculateRadius(progress);int color = calculateColor(progress);paint.setColor(color);canvas.drawCircle(pos[0], pos[1], radius, paint);distance += step;}}/*** 绘制起点和终点*/private void drawEndPoints(Canvas canvas) {// 起点paint.setColor(startColor);canvas.drawCircle(points.get(0).x, points.get(0).y, maxRadius + 5, paint);// 终点paint.setColor(endColor);canvas.drawCircle(points.get(points.size() - 1).x,points.get(points.size() - 1).y,minRadius + 2,paint);}private float calculateRadius(float progress) {// 使用缓动函数float easedProgress = (float) (1 - Math.pow(1 - progress, 1.5));return maxRadius - (maxRadius - minRadius) * easedProgress;}private int calculateColor(float progress) {return interpolateColor(startColor, endColor, progress);}private int interpolateColor(int color1, int color2, float factor) {float inverseFactor = 1 - factor;return Color.argb((int) (Color.alpha(color1) * inverseFactor + Color.alpha(color2) * factor),(int) (Color.red(color1) * inverseFactor + Color.red(color2) * factor),(int) (Color.green(color1) * inverseFactor + Color.green(color2) * factor),(int) (Color.blue(color1) * inverseFactor + Color.blue(color2) * factor));}public void addPoint(float x, float y) {points.add(new PointF(x, y));invalidate();}public void clearPoints() {points.clear();path.reset();invalidate();}public void setColors(int startColor, int endColor) {this.startColor = startColor;this.endColor = endColor;}
}
看着效果还可以,我在这里画的是圆,当然也可以绘制其他的形状来实现笔锋效果,比如说线和图片。
画线效果:
画图片效果:可以达到类似水彩笔的效果