Android關於Path你所知道的和不知道的一切

張風捷特烈發表於2018-11-07

零、前言

1.canvas本身提供了很多繪製基本圖形的方法,普通繪製基本滿足
2.但是更高階的繪製canvas便束手無策,但它的一個方法卻將圖形的繪製連線到了另一個次元
3.下面進入Path的世界,[注]:本文只說Path,關於繪製只要使用Canvas.drawPath(Path,Paint)即可
4.本文將對Path的所有API進行測試。


一、引:認識Path

例1.繪製網格

在Canvas篇我用Path畫過一個網格輔助,在這裡分析一下
moveTo相當於抬筆到某點,lineTo表示畫下到某點

    /**
     * 繪製網格:注意只有用path才能繪製虛線
     *
     * @param step    小正方形邊長
     * @param winSize 螢幕尺寸
     */
    public static Path gridPath(int step, Point winSize) {
        //建立path
        Path path = new Path();
        //每間隔step,將筆點移到(0, step * i),然後畫線到(winSize.x, step * i)
        for (int i = 0; i < winSize.y / step + 1; i++) {
            path.moveTo(0, step * i);
            path.lineTo(winSize.x, step * i);
        }

        for (int i = 0; i < winSize.x / step + 1; i++) {
            path.moveTo(step * i, 0);
            path.lineTo(step * i, winSize.y);
        }
        return path;
    }
複製程式碼
//準備畫筆
mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRedPaint.setColor(Color.RED);
mRedPaint.setStrokeWidth(2);
mRedPaint.setStyle(Paint.Style.STROKE);
//設定虛線效果new float[]{可見長度, 不可見長度},偏移值
mRedPaint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0)); 

//繪製
Path path = HelpPath.gridPath(50, mWinSize);
canvas.drawPath(path, mRedPaint);
複製程式碼

path畫線.png


例2.繪製N角星

曾經花了半天研究五角星的構造,通過兩個圓,發現了N角星繪製的通法
又用半天用JavaScript的Canvas實現了在瀏覽器上的繪製,當然Android也不示弱:

mmexport1541469593236.jpg

1).通用n角星路徑繪製:(基本上都是一些點位和角度的計算,然後連線)
/**
 * n角星路徑
 *
 * @param num 幾角星
 * @param R   外接圓半徑
 * @param r   內接圓半徑
 * @return n角星路徑
 */
public static Path nStarPath(int num, float R, float r) {
    Path path = new Path();
    float perDeg = 360 / num;
    float degA = perDeg / 2 / 2;
    float degB = 360 / (num - 1) / 2 - degA / 2 + degA;
    path.moveTo(
            (float) (Math.cos(rad(degA + perDeg * 0)) * R + R * Math.cos(rad(degA))),
            (float) (-Math.sin(rad(degA + perDeg * 0)) * R + R));
    for (int i = 0; i < num; i++) {
        path.lineTo(
                (float) (Math.cos(rad(degA + perDeg * i)) * R + R * Math.cos(rad(degA))),
                (float) (-Math.sin(rad(degA + perDeg * i)) * R + R));
        path.lineTo(
                (float) (Math.cos(rad(degB + perDeg * i)) * r + R * Math.cos(rad(degA))),
                (float) (-Math.sin(rad(degB + perDeg * i)) * r + R));
    }
    path.close();
    return path;
}   

/**
 * 角度制化為弧度制
 *
 * @param deg 角度
 * @return 弧度
 */
public static float rad(float deg) {
    return (float) (deg * Math.PI / 180);
}
複製程式碼
2).當外接圓和內切圓的半徑成一定的關係,可形成正多角星,和正多邊形

正多角星:

    /**
     * 畫正n角星的路徑:
     *
     * @param num 角數
     * @param R   外接圓半徑
     * @return 畫正n角星的路徑
     */
    public static Path regularStarPath(int num, float R) {
        float degA, degB;
        if (num % 2 == 1) {//奇數和偶數角區別對待
            degA = 360 / num / 2 / 2;
            degB = 180 - degA - 360 / num / 2;
        } else {
            degA = 360 / num / 2;
            degB = 180 - degA - 360 / num / 2;
        }
        float r = (float) (R * Math.sin(rad(degA)) / Math.sin(rad(degB)));
        return nStarPath(num, R, r);
    }
複製程式碼

正多邊形:

    /**
     * 畫正n邊形的路徑
     *
     * @param num 邊數
     * @param R   外接圓半徑
     * @return 畫正n邊形的路徑
     */
    public static Path regularPolygonPath(int num, float R) {
        float r = (float) (R * (Math.cos(rad(360 / num / 2))));//!!一點解決
        return nStarPath(num, R, r);
    }

    /**
     * 角度制化為弧度制
     *
     * @param deg 角度
     * @return 弧度
     */
    public static float rad(float deg) {
        return (float) (deg * Math.PI / 180);
    }
複製程式碼

n角星

這兩個小栗子作為引,應該對Path的能為有一定的瞭解了吧,下面將正式對Path做系統地介紹


二、Path的詳細介紹

Path定位: 是一個類,直接繼承自Object,原始碼行數879(一盞茶的功夫就看完了),算個小類
native方法很多,說明它跟底層打交道的,感覺不好惹
下面看一下Path的公共方法:(基本建立相關、新增相關、設定相關,其他)
注:為了好看,以下所有演示為橫屏且canvas的座標原點移至(800,500),所有藍線為輔助線

Path一覽.png


1.moveTo----lineTo----close

moveTo:抬筆到某點
lineTo:畫線到某點
close:閉合首位

Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 200);
複製程式碼

畫線.png

Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 200);
path.lineTo(200, 100);
複製程式碼

畫線2.png

Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 200);
path.lineTo(200, 100);
path.close();
複製程式碼

close.png


2.rMoveTo----rLineTo

rMoveTo:從路徑尾部為起點,抬筆
rLineTo:從路徑尾部為起點,畫直線
其實也不難理解,就是點的參考系從canvas左上角移變成路徑尾部,看一下就知道了:

Path path = new Path();
path.rMoveTo(0,0);
path.rLineTo(100, 200);
path.rLineTo(200, 100);
path.close();
複製程式碼

rlineto.png


3.繪製弧:arcTo(矩形範圍,起點,終點,)
RectF rectF = new RectF(100, 100, 500, 300);
path.moveTo(0, 0);
//arcTo(矩形範圍,起點,終點,是否獨立--預設false)
//path.arcTo(rectF, 0, 45, true);
path.arcTo(rectF, 0, 45, false);

複製程式碼

繪製弧線.png

剩下的貝塞爾曲線這個大頭放在本篇最後


三、路徑新增:addXXX

可以看出齊刷刷的Direction,先看看它是什麼鬼:
是一個列舉,只有CW(順時針)和CCW(逆時針),這裡暫且按下,都使用CW,後文詳述:

    public enum Direction {
        /** clockwise */
        CW  (0),    // must match enum in SkPath.h---順時針
        /** counter-clockwise */
        CCW (1);    // must match enum in SkPath.h---逆時針

        Direction(int ni) {
            nativeInt = ni;
        }
        final int nativeInt;
    }
複製程式碼

1.加矩形路徑:
1).普通矩形:addRect(左,上,右,下)
RectF rectF = new RectF(100, 100, 500, 300);
path.addRect(rectF, Path.Direction.CW);//順時針畫矩形
複製程式碼
2).圓角矩形:addRoundRect(矩形域,圓角x,圓角y)
RectF rectF = new RectF(100, 100, 500, 300);
path.addRoundRect(rectF, 50, 50, Path.Direction.CW);//順時針畫圓角矩形
複製程式碼
3).用4點控制圓角:addRoundRect(矩形域,8數,方向)
RectF rectF = new RectF(100, 100, 500, 300);
path.addRoundRect(rectF, new float[]{
        150, 150,//左上圓角x,y
        0, 0,//右上圓角x,y
        450, 250,//右下圓角x,y
        250, 200//左下圓角x,y
}, Path.Direction.CW);//順時針畫
複製程式碼

矩形相關.png


2.加橢圓路徑:addOval(矩形域,方向)
RectF rectF = new RectF(100, 100, 500, 300);
path.addOval(rectF, Path.Direction.CW);
複製程式碼

繪製橢圓.png


3.加圓路徑:addCircle(圓心x,圓心y,方向)
path.addCircle(100,100,100,Path.Direction.CW);
複製程式碼

圓.png


4.加弧線路徑:addArc(矩形域,起始角度終止角度)
RectF rectF = new RectF(100, 100, 500, 300);
path.addArc(rectF,0,145);
複製程式碼

弧線.png

5.新增路徑:
1).普通新增addPath(Path)
path.addCircle(100,100,100,Path.Direction.CW);
Path otherPath = new Path();
otherPath.moveTo(0, 0);
otherPath.lineTo(100, 100);
path.addPath(otherPath);
複製程式碼
2).偏移新增:addPath(Path,偏移x,偏移y)
path.addCircle(100,100,100,Path.Direction.CW);
Path otherPath = new Path();
otherPath.moveTo(0, 0);
otherPath.lineTo(100, 100);
path.addPath(otherPath,200,200);
複製程式碼
3).矩陣變換新增:addPath(Path,Matrix)
path.addCircle(100,100,100,Path.Direction.CW);
Path otherPath = new Path();
otherPath.moveTo(0, 0);
otherPath.lineTo(100, 100);

Matrix matrix = new Matrix();
matrix.setValues(new float[]{
        1, 0, 100,
        0, .5f, 150,
        0, 0, 1
});
path.addPath(otherPath, matrix);
複製程式碼

新增路徑.png


四、其他操作:
1.細碎小點綜述:
        path.reset();//清空path,保留填充型別
        //path.rewind();//清空path,保留資料結構
        path.isEmpty()//是否為空
        path.isRect(new RectF());
        path.isConvex();
        path.isInverseFillType();

        path.set(otherPath);//清空path後新增新Path
//        path.offset(200,200);//平移
//        path.transform(matrix);//矩陣變換

        Path tempPath = new Path();
//        path.offset(200, 200, tempPath);//基於path平移注入tempPath,path不變
        path.transform(matrix, tempPath);//基於path變換注入tempPath,path不變

        canvas.drawPath(path, mRedPaint);
        canvas.drawPath(tempPath, mRedPaint);
複製程式碼
2.順時針CW和逆時針CCW的區別
1).setLastPoint(x,y):設定最後一點

Path相當於將點按順序儲存,setLastPoint(x,y)方法則是將最後一個點換掉

RectF rectF = new RectF(100, 100, 500, 300);
path.addRect(rectF, Path.Direction.CW);//順時針畫矩形
path.setLastPoint(200, 200);
canvas.drawPath(path, mRedPaint);
複製程式碼

順時針.png

RectF rectF = new RectF(100, 100, 500, 300);
path.addRect(rectF, Path.Direction.CCW);//順時針畫矩形
path.setLastPoint(200, 200);
canvas.drawPath(path, mRedPaint);
複製程式碼

逆時針.png

3.邊界計算:
Path starPath = CommonPath.nStarPath(6, 100, 50);
RectF rectF = new RectF();//自備矩形區域
starPath.computeBounds(rectF, true);
canvas.drawPath(starPath, mRedPaint);
canvas.drawRect(rectF,mHelpPaint);
複製程式碼

檢視矩形路徑區域.png

五、路徑的填充

1.初識路徑的填充:
1)左圖:兩個都是順時針:
mRedPaint.setStyle(Paint.Style.FILL);
RectF rectF = new RectF(100, 100, 500, 300);
path.addRect(rectF, Path.Direction.CW);//順時針畫矩形
path.addRect(200, 0, 400, 400, Path.Direction.CW);//順時針畫矩形
複製程式碼
2)右圖:橫的順時針,豎的逆時針
mRedPaint.setStyle(Paint.Style.FILL);
RectF rectF = new RectF(100, 100, 500, 300);
path.addRect(rectF, Path.Direction.CW);//順時針畫矩形
path.addRect(200, 0, 400, 400, Path.Direction.CCW);//逆時針畫矩形
複製程式碼

填充.png

感覺向兩個水渦,同向加劇,反向中間就抵消了

2.填充的環繞原則:---在自然科學(如數學,物理學)中的概念

非零環繞原則(WINDING)----預設
反零環繞原則(INVERSE_WINDING)
奇偶環繞原則(EVEN_ODD)
反奇偶環繞原則(INVERSE_EVEN_ODD)

  public enum FillType {
        WINDING         (0),
        EVEN_ODD        (1),
        INVERSE_WINDING (2),
        INVERSE_EVEN_ODD(3);
        FillType(int ni) {
            nativeInt = ni;
        }
        final int nativeInt;
    }
複製程式碼
Path.FillType fillType = path.getFillType();//獲取型別
path.setFillType(Path.FillType.XXXXXX)//設定型別
複製程式碼
//繪製的測試五角星
path.moveTo(100, 200);
path.lineTo(500, 200);
path.lineTo(200, 400);
path.lineTo(300, 50);
path.lineTo(400, 400);
path.close();
複製程式碼

1).非零環繞數規則:WINDING

根據我個人的理解(僅供參考):在非零環繞數規則下

判斷一點在不在圖形內:從點引射線P,
遇到順時針邊+1
遇到逆時針邊-1
結果0,不在,否則,在
複製程式碼

非零環繞.png


2).奇偶環繞數規則:EVEN_ODD

根據我個人的理解(僅供參考):奇偶環繞數規則

判斷一點在不在圖形內(非定點):
從點引射線P,看與圖形交點個數
奇數在,偶數,不在
複製程式碼

奇偶環繞.png

3).反非零環繞數規則和反奇偶環繞數規則:

就是和上面相比,該填充的不填充,不填充的填充

反環繞.png

這樣看來圖形的順時針或逆時針繪製對於填充是非常重要的
綜合來說奇偶原則比較簡單粗暴,但非零原則作為預設方式體現了它的通用性


六、布林運算OP:(兩個路徑之間的運算)

如果說環繞原則是一個Path的自我糾結,那麼OP就是兩個路徑之間的勾心鬥角

Path right = new Path();
Path left = new Path();
left.addCircle(0, 0, 100, Path.Direction.CW);
right.addCircle(100, 0, 100, Path.Direction.CW);
//left.op(right, Path.Op.DIFFERENCE);//差集----暈,咬了一口硫酸
//left.op(right, Path.Op.REVERSE_DIFFERENCE);//反差集----賠了夫人又折兵
//left.op(right, Path.Op.INTERSECT);//交集----與你不同的都不是我
//left.op(right, Path.Op.UNION);//並集----在一起,在一起
left.op(right, Path.Op.XOR);//異或集---我恨你,我也恨你

canvas.drawPath(left, mRedPaint);
複製程式碼

op.png


七、Path動畫:PathMeasure

init方法裡:
//測量路徑
PathMeasure pathMeasure = new PathMeasure(mStarPath, false);
//使用ValueAnimator
ValueAnimator pathAnimator = ValueAnimator.ofFloat(1, 0);
pathAnimator.setDuration(5000);
pathAnimator.addUpdateListener(animation -> {
    float value = (Float) animation.getAnimatedValue();
    //使用畫筆虛線效果+偏移
    DashPathEffect effect = new DashPathEffect(
            new float[]{pathMeasure.getLength(), pathMeasure.getLength()},
            value * pathMeasure.getLength());
    mRedPaint.setPathEffect(effect);
    invalidate();
});
pathAnimator.start();
複製程式碼
OnDraw方法裡:
canvas.drawPath(mStarPath, mRedPaint);
複製程式碼

路徑動畫.gif


八、貝塞爾曲線簡述:

如果說Path是Canvas為了高階繪製留下的窗子那麼貝塞爾曲線則Path為了更高階的繪製而留下的門
由於操作的複雜性,這裡並不過渡深入,以後有需求的話會專門開一篇

1.簡單認識:(圖來源網路)
一階貝塞爾 二階貝塞爾 三階貝塞爾
Android關於Path你所知道的和不知道的一切
Android關於Path你所知道的和不知道的一切
Android關於Path你所知道的和不知道的一切
2.二階貝塞爾曲線示例:
public class Bezier2View extends View {
    private Paint mHelpPaint;//輔助畫筆
    private Paint mPaint;//貝塞爾曲線畫筆
    private Path mBezierPath;//貝塞爾曲線路徑
    //起點
    private PointF start = new PointF(0, 0);
    //終點
    private PointF end = new PointF(400, 0);
    //控制點
    private PointF control = new PointF(200, 200);
    private Picture mPicture;//座標系和網格的Canvas元件
    private Point mCoo;//座標系
    public Bezier2View(Context context) {
        this(context, null);
    }

    public Bezier2View(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        //貝塞爾曲線畫筆
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.parseColor("#88EC17F3"));
        mPaint.setStrokeWidth(8);
        //輔助線畫筆
        resetHelpPaint();
        recordBg();//初始化時錄製座標系和網格--避免在Ondraw裡重複呼叫
        mBezierPath = new Path();
    }

    /**
     * 初始化時錄製座標系和網格--避免在Ondraw裡重複呼叫
     */
    private void recordBg() {
        //準備螢幕尺寸
        Point winSize = new Point();
        mCoo = new Point(800, 500);
        Utils.loadWinSize(getContext(), winSize);
        Paint gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPicture = new Picture();
        Canvas recordCanvas = mPicture.beginRecording(winSize.x, winSize.y);
        //繪製輔助網格
        HelpDraw.drawGrid(recordCanvas, winSize, gridPaint);
        //繪製座標系
        HelpDraw.drawCoo(recordCanvas, mCoo, winSize, gridPaint);
        mPicture.endRecording();
    }

    /**
     * 重置輔助畫筆
     */
    private void resetHelpPaint() {
        mHelpPaint = new Paint();
        mHelpPaint.setColor(Color.BLUE);
        mHelpPaint.setStrokeWidth(2);
        mHelpPaint.setStyle(Paint.Style.STROKE);
        mHelpPaint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));
        mHelpPaint.setStrokeCap(Paint.Cap.ROUND);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根據觸控位置更新控制點,並提示重繪
        control.x = event.getX() - mCoo.x;
        control.y = event.getY() - mCoo.y;
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(mCoo.x, mCoo.y);
        drawHelpElement(canvas);//繪製輔助工具--控制點和基準選
        // 繪製貝塞爾曲線
        mBezierPath.moveTo(start.x, start.y);
        mBezierPath.quadTo(control.x, control.y, end.x, end.y);
        canvas.drawPath(mBezierPath, mPaint);
        mBezierPath.reset();//清空mBezierPath
        canvas.restore();
        canvas.drawPicture(mPicture);
    }
    /**
     * 繪製輔助工具--控制點和基準選
     *
     * @param canvas
     */
    private void drawHelpElement(Canvas canvas) {
        // 繪製資料點和控制點
        mHelpPaint.setColor(Color.parseColor("#8820ECE2"));
        mHelpPaint.setStrokeWidth(20);
        canvas.drawPoint(start.x, start.y, mHelpPaint);
        canvas.drawPoint(end.x, end.y, mHelpPaint);
        canvas.drawPoint(control.x, control.y, mHelpPaint);
        // 繪製輔助線
        resetHelpPaint();
        canvas.drawLine(start.x, start.y, control.x, control.y, mHelpPaint);
        canvas.drawLine(end.x, end.y, control.x, control.y, mHelpPaint);
    }
}
複製程式碼

效果如下:(模擬器+錄屏軟體+AS有點卡,手機上演示很流暢的)

二階貝塞爾.gif


3.三階貝塞爾的簡單演示:

mRedPaint.setStrokeWidth(5);
mRedPaint.setStrokeCap(Paint.Cap.ROUND);
path.moveTo(0, 0);//定點1_x,定點1_y
//(控制點1_X,控制點1_y,控制點2_x,控制點2_y,定點2_x,定點2_y)
path.cubicTo(100, 100, 300, -300, 600, 0);
複製程式碼

三階貝塞爾.png

好了,Path完結散花


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--無 2018-11-6 Android關於Path你所知道的和不知道的一切
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的CSDN 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章