????????上一章講解了如何通過多個 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;}
}
看著效果還可以,我在這里畫的是圓,當然也可以繪制其他的形狀來實現筆鋒效果,比如說線和圖片。
畫線效果:
畫圖片效果:可以達到類似水彩筆的效果