美文网首页自定义控件
高级UI<第二十九篇>:Android开发之Path详解

高级UI<第二十九篇>:Android开发之Path详解

作者: NoBugException | 来源:发表于2020-01-07 02:49 被阅读0次
    (1)定义

    Path顾名思义就是路径的意思,也可以说是轨迹的意思,Path可以帮助view完成一些复杂的动画效果。

    (2)基本方法
    作用 相关方法 备注
    移动起点 moveTo 移动下一次操作的起点位置
    设置终点 setLastPoint 重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同
    连接直线 lineTo 添加上一个点到当前点之间的直线到Path
    闭合路径 close 连接第一个点连接到最后一个点,形成一个闭合区域
    添加内容 addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo 添加(矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前Path (注意addArc和arcTo的区别)
    是否为空 isEmpty 判断Path是否为空
    是否为矩形 isRect 判断path是否是一个矩形
    替换路径 set 用新的路径替换到当前路径所有内容
    偏移路径 offset 对当前路径之前的操作进行偏移(不会影响之后的操作)
    贝塞尔曲线 quadTo, cubicTo 分别为二次和三次贝塞尔曲线的方法
    rXxx方法 rMoveTo, rLineTo, rQuadTo, rCubicTo 不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)
    填充模式 setFillType, getFillType, isInverseFillType, toggleInverseFillType 设置,获取,判断和切换填充模式
    提示方法 incReserve 提示Path还有多少个点等待加入(这个方法貌似会让Path优化存储结构)
    布尔操作(API19) op 对两个Path进行布尔运算(即取交集、并集等操作)
    计算边界 computeBounds 计算Path的边界
    重置路径 reset, rewind 清除Path中的内容,reset不保留内部数据结构,但会保留FillType。rewind会保留内部的数据结构,但不保留FillType
    矩阵操作 transform 矩阵变换
    (3)Paint配置
        private void init(Context mContext){
            mPaint = new Paint();
            mPaint.setColor(Color.BLACK);
            mPaint.setStrokeWidth(10);
            mPaint.setStrokeCap(Paint.Cap.ROUND);
            mPaint.setStyle(Paint.Style.STROKE);
        }
    

    这里需要注意的是:

    • 如果绘制非闭合图形,务必将画笔设置成Paint.Style.STROKE描边模式,否则绘制无效。
    • 如果绘制闭合图形,可以使用三种模式:Paint.Style.STROKEPaint.Style.FILLPaint.Style.FILL_AND_STROKE,分别是描边模式、填充模式、描边并填充模式。
    (4)moveTo和lineTo

    lineTo(x, y):绘制一条执行。

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.lineTo(200, 200);
            canvas.drawPath(path, mPaint);
    

    lineTo的两个传参是确定某一点的位置,那么两点确定一条直线,这里还有一个点是什么呢?

    如下图所示,

    图片.png

    我们的画笔默认位置是(0, 0),如上图所示,(0, 0)的位置就在红色矩形区域里面的小红点位置,也就是说,画笔从(0, 0)到(200, 200)绘制直线,这样就满足了两点确定一条直线的理念,绘制之后的效果图如下:

    图片.png

    PathmoveTo方法可以指定画笔的位置,也就是下次绘制的开始位置,下面我们结合moveTo画直线。我们现在画一个假直角坐标系,将画笔位置移动到原点,并绘制直线。

            mPaint.setColor(Color.BLACK);
    
            //绘制一个假直角坐标系
            canvas.drawLine(0, 800, canvas.getWidth(), 800, mPaint);
            canvas.drawLine(canvas.getWidth() / 2, 0, canvas.getWidth() / 2, canvas.getHeight(),mPaint);
    
            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.moveTo(canvas.getWidth() / 2, 800);
            path.lineTo(200, 200);
            canvas.drawPath(path, mPaint);
    
    图片.png
    (5)moveTosetLastPoint

    绘制两条直线

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.moveTo(canvas.getWidth() / 2, 800);
            path.lineTo(200, 200);
            path.moveTo(50, 200);
            path.lineTo(100,600);
            canvas.drawPath(path, mPaint);
    

    代码分析:

    • 期初画笔位置是(0, 0),执行path.moveTo(canvas.getWidth() / 2, 800)之后画笔位置变成了(canvas.getWidth() / 2, 800);
    • lineTo(200, 200): 绘制直线,画笔将从(canvas.getWidth() / 2, 800)开始画直线,直到(200, 200)停下;
    • moveTo(50, 200): 画笔将从(200, 200)移动到(50, 200);
    • lineTo(100,600): 绘制直线,画笔将从(50, 200)开始画直线,直到(100,600)停下;
    • drawPath: 开始绘制,这一步才开始绘制,前面的只是设置轨迹而已。

    效果图:

    图片.png

    下面开始解释下setLastPointsetLastPoint的意思就是重置最近一次画笔位置。

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.moveTo(canvas.getWidth() / 2, 800);
            path.lineTo(200, 200);
            path.setLastPoint(50, 200);
            path.lineTo(100,600);
            canvas.drawPath(path, mPaint);
    

    代码分析:

    • 期初画笔位置是(0, 0),执行path.moveTo(canvas.getWidth() / 2, 800)之后画笔位置变成了(canvas.getWidth() / 2, 800);
    • lineTo(200, 200): 绘制直线,画笔将从(canvas.getWidth() / 2, 800)开始画直线,直到(200, 200)停下;
    • setLastPoint(50, 200): 此时画笔的位置是(200, 200),setLastPoint将重置画笔的位置,使得上一次画笔的位置变成了(50, 200);
    • lineTo(100,600): 绘制直线,画笔将从(50, 200)开始画直线,直到(100,600)停下;
    • drawPath: 开始绘制,这一步才开始绘制,前面的只是设置轨迹而已。

    效果图如下:

    图片.png
    (6)close

    Path有个close方法,可以将第一个点和第二个点相连,形成闭合区域。

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.moveTo(canvas.getWidth() / 2, 800);
            path.lineTo(200, 200);
            path.lineTo(100,600);
            path.close();
            canvas.drawPath(path, mPaint);
    

    如图所示

    图片.png

    如果是Paint的样式修改成Paint.Style.FILL或者Paint.Style.FILL_AND_STROKE,那么可以不需要执行close()也可以达到闭合效果。

    图片.png
    (7)addXXX系列
    图片.png

    这些方法大致都是添加路劲(弧路径圆路径椭圆路径矩形路径圆角矩形路径等等)

    这里唯一需要说明的是Path.Direction.CWPath.Direction.CCW

    一些方法中有个参数:
    Path.Direction.CW: 顺时针
    Path.Direction.CCW: 逆时针

    我们来画一个圆

    顺时针:

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.addCircle(0, 0, 200, Path.Direction.CW);
            canvas.drawPath(path, mPaint);
    

    如图:

    图片.png

    画笔起始点是(200, 0),结束点是(0, -200)。

    那么, 我们可以利用setLastPoint来画一个桃子

            path.setLastPoint(200, -200);
    
    图片.png

    逆时针:

    path.addCircle(0, 0, 200, Path.Direction.CCW);
    

    起始点是(200, 0),结束点是(0, 200),桃子在下面

    path.setLastPoint(200, 200);
    
    图片.png

    其它的路径就不举例了,总之,如果是闭合区域,我们首先需要确定的是顺时针还是逆时针,进而推敲出路径的起始点和结束点。

    (8)addArcarcTo

    addArc:直接添加一个圆弧到path中

    arcTo: 直接添加一个圆弧到path中,并且将当前路径的起始点和上一个路径的结束点连接。

    arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) 
    arcTo(RectF oval, float startAngle, float sweepAngle)
    arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
    

    addArcarcTo都是圆弧,这里主要展示一下它们的区别?

    使用lineTo和addArc绘制直线和圆弧

            path.lineTo(100, 100);
            RectF rectF = new RectF();
            rectF.left = 150;
            rectF.top = 100;
            rectF.right = 300;
            rectF.bottom = 200;
            path.addArc(rectF, 30,60);
            canvas.drawPath(path, mPaint);
    
    图片.png

    我们发现直线和圆弧互不相连。现在将addArc替换成arcTo

            path.lineTo(100, 100);
            RectF rectF = new RectF();
            rectF.left = 150;
            rectF.top = 100;
            rectF.right = 300;
            rectF.bottom = 200;
            path.arcTo(rectF, 30, 60);
            canvas.drawPath(path, mPaint);
    
    图片.png

    我们发现,当前圆弧路径的起始点和上一个路径的终点相连了。

    在方法里面有个参数forceMoveTo

    true: 不相连,相当于addArc
    false: 当前圆弧路径的起始点和上一个路径的终点相连;

    (9) computeBounds

    计算path的边界。

    computeBounds(RectF bounds, boolean exact)
    

    bounds:矩形边界
    exact:这个参数已经没用了

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            path.lineTo(200, 200);
            path.moveTo(-200, -200);
            path.lineTo(-300,100);
            RectF rectF = new RectF();
            path.computeBounds(rectF, false);
            path.addRect(rectF, Path.Direction.CW);
            canvas.drawPath(path, mPaint);
    

    效果图如下:

    图片.png

    代码中画了两条线段,然后画了一个矩形,但是奇怪的是,这个矩形没有设置任何边界大小,computeBounds是一个很有意思的方法,它可以自动计算两条线段所在的边界范围,所以当绘制这个矩形的时候其边界不是(0,0,0,0),当Path的轨迹所占数量为0或者1时,绘制这个矩形的时候其边界是(0,0,0,0)。

    (10)incReserve(int extraPtCount)

    提示路径以准备添加更多点。这可以允许更有效地分配其存储的路径。

    extraPtCount: 可以添加到这个的额外点数

    (11)isEmpty()

    判断Path的路径是否为空,如果Path没有路径,则说明Path的路径是空的。

    (12)isRect

    判断Path是否是矩形路径。

            Path path = new Path();
            path.lineTo(0, 0);
            path.lineTo(200, 0);
            path.lineTo(200, 200);
            path.lineTo(0,200);
            boolean isRect = path.isRect(rectF);
            //path.addRect(rectF, Path.Direction.CW);
            canvas.drawPath(path, mPaint);
    

    computeBounds有点类似,都是计算当前路径的边界,但是又和 computeBounds不同:

    • isRect只是判断当前Path的路径是否是矩形;
    • isRect传递一个rectF参数,如果返回true,则被计算之后的rectF和computeBounds效果一样,添加path.addRect(rectF, Path.Direction.CW)同样可以绘制出矩形边界;
    • isRect传递一个rectF参数,如果返回false(Path的路径非矩形),rectF大小就是(0,0,0,0),此时rectF将被忽略,如果这时再添加path.addRect(rectF, Path.Direction.CW),rectF将不再被忽略,rectF将被绘制出来,由于rectF的大小是(0,0,0,0),所以之前的非矩形路径随之被隐藏。
    (13)isConvex()

    判断曲线是否具有凸性。

    首先我们绘制两条直线,两条直线的结束点和起始点相连,如图所示

    图片.png

    其中(0,0)我们称之为曲线的拐点,下面我们设置一下Path效果,让这个曲线更像一个曲线吧

            mPaint.setPathEffect(new CornerPathEffect(200));
    
    图片.png

    定义: 如果曲线上任意两点都在曲线的方,则这个曲线具有上凸特性。

    我们再画一个曲线,如下图:

    图片.png

    定义: 如果曲线上任意两点都在曲线的方,则这个曲线具有下凸特性。

    再画一个曲线,如下图:

    图片.png

    像这样的曲线既不满足上凸的特性,也不满足下凸的特性,所以该曲线没有凸性

    再画一个,如下图:

            RectF rect = new RectF(0,0,400,400);
            path.addRect(rect, Path.Direction.CCW);
    
    图片.png

    那么这个矩形是否符合凸性呢?

    想要搞清楚这个问题,必须搞清楚凸性的起点和终点,我们可以通过setLastPoint方法来找出凸性的起点和终点。

    第一次实验:

            RectF rect = new RectF(0,0,400,400);
            path.addRect(rect, Path.Direction.CCW);
    
            path.setLastPoint(100, 200);
    
    图片.png

    由于矩形是按照逆时针的方式绘制,并且setLastPoint之后图形变成上图的样子,那么可以证明,图形的起始点(0,0),原来终点是(400,0),现在终点是(100,200),由于矩形是闭合区域所以起点终点相连之后形成了闭合图形,现在我们不让它闭合,擦除多余的部分:

    图片.png

    此时,该曲线完全符合上凸特性,我们称之为,当矩形按照逆时针绘制后的图形,具有凸性

    第二次试验:

            RectF rect = new RectF(0,0,400,400);
            path.addRect(rect, Path.Direction.CW);
    
            path.setLastPoint(200, 100);
    
    图片.png

    由于矩形是按照顺时针的方式绘制,并且setLastPoint之后图形变成上图的样子,那么可以证明,图形的起始点(0,0),原来终点是(0,400),现在终点是(200,100),由于矩形是闭合区域所以起点终点相连之后形成了闭合图形,现在我们不让它闭合,擦除多余的部分:

    图片.png

    此时,该曲线完全符合下凸特性,我们称之为,当矩形按照顺时针绘制后的图形,具有凸性

    isConvex总结:
    • isConvex是API 21新增的接口,其使用量也相对较少;
    • 判断一个图形是否具有凸性,并不是靠猜,而是有方法的;
    • 需要对数学几何的上凸下凸的特性具有一定的了解;
    • 需要找到图形的起点终点,如果是封闭区域,需要擦除起点终点连接(path.close())的区域,让图形变成不再闭合,最终判断曲线是否符合凸性
    • 可以结合setLastPoint方法寻找图形的起点终点
    • 上面判断图形是否具有凸性的方法写的很明白了,其它图形(比如:圆) 也可通过这个方法判断是否具有凸性
    (14)setFillTypeisInverseFillType

    setFillType: 设置Path的填充类型,指定内部的计算方式;

    其填充类型有:
    FillType .WINDING: 填充每一个封闭路径。
    FillType .EVEN_ODD: 填充每个封闭路径不重合的地方。
    FillType .INVERSE_WINDING:WINDING相反,WINDING填充每个封闭路径的内部,而INVERSE_WINDING填充每个封闭路径的外部空间。
    FillType .INVERSE_EVEN_ODD:EVEN_ODD相反,它填充封闭路径之外的空间和路径和路径相交的空间。

    演示这些填充类型之前,需要将Paint的样式改为填充模式

            mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    

    或者

            mPaint.setStyle(Paint.Style.FILL);
    
    • FillType.WINDING
    图片.png
    • FillType.EVEN_ODD
      图片.png
    • FillType.INVERSE_WINDING
    图片.png
    • FillType.INVERSE_EVEN_ODD
    图片.png

    isInverseFillType: 判断是否为反转填充类型。

    反转填充类型有两种FillType.INVERSE_WINDINGFillType.INVERSE_EVEN_ODD,我们看一下源码

    /**
     * Returns true if the filltype is one of the INVERSE variants
     *
     * @return true if the filltype is one of the INVERSE variants
     */
    public boolean isInverseFillType() {
        final int ft = nGetFillType(mNativePath);
        return (ft & FillType.INVERSE_WINDING.nativeInt) != 0;
    }
    

    核心语句是(ft & FillType.INVERSE_WINDING.nativeInt) != 0;,核心算法是按位与计算取值范围,按位与(&)使数字分组:

    第一组: 取值范围是 20
    第二组: 取值范围是 [21,22
    第三组: 取值范围是 [22,23
    第四组: 取值范围是 [23,24
    依次类推...

    算法特性: 同一范围内的两数的&运算,等于当前范围的最小数(比如5&6=4),不同范围的&运算结果为0;

    根据这个算法特性,我们再来看下代码;
    分析:

    • 填充类型取值分别是:0,1,2,3
    • FillType.INVERSE_WINDING的取值是2,FillType.INVERSE_EVEN_ODD的取值是3,这两个反转填充类型的取值正好都在第二组范围。(我想接下来不需要我解释了吧)
    (15)set(Path src)

    将原有Path,替换为src。

    (16)offset

    将Path平移到指定点。

    offset(float dx, float dy)
    offset(float dx, float dy, @Nullable Path dst)
    

    offset有两个方法。

    方法一:

            Path path = new Path();
            path.addCircle(0,0,200, Path.Direction.CW);
            path.offset(100, 100);
            canvas.drawPath(path, mPaint);
    

    效果如下:


    图片.png

    方法二:

            Path path = new Path();
            path.addCircle(0,0,200, Path.Direction.CW);
            Path path1 = new Path();
            path.offset(100, 100, path1);
            canvas.drawPath(path, mPaint);
            canvas.drawPath(path1, mPaint);
    

    效果如下:

    图片.png
    (17)Op
    op(Path path, Op op)
    op(Path path1, Path path2, Op op)
    

    组合两条路径时可以执行的逻辑操作。

    逻辑操作有:

    Op.DIFFERENCE: 从第一条路径中减去第二条路径。

            Path path = new Path();
            path.addCircle(-100,-100,200, Path.Direction.CW);
            Path newPath = new Path();
            newPath.addCircle(100,100,200, Path.Direction.CW);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                path.op(newPath, Path.Op.DIFFERENCE);
            }
            canvas.drawPath(path, mPaint);
    
    图片.png

    Op.INTERSECT: 两条路径相交。

            Path path = new Path();
            path.addCircle(-100,-100,200, Path.Direction.CW);
            Path newPath = new Path();
            newPath.addCircle(100,100,200, Path.Direction.CW);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                path.op(newPath, Path.Op.INTERSECT);
            }
            canvas.drawPath(path, mPaint);
    
    图片.png

    Op.UNION: 把这两条路联合起来。

            Path path = new Path();
            path.addCircle(-100,-100,200, Path.Direction.CW);
            Path newPath = new Path();
            newPath.addCircle(100,100,200, Path.Direction.CW);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                path.op(newPath, Path.Op.UNION);
            }
            canvas.drawPath(path, mPaint);
    
    图片.png

    Op.XOR: 排他或两条路。

            Path path = new Path();
            path.addCircle(-100,-100,200, Path.Direction.CW);
            Path newPath = new Path();
            newPath.addCircle(100,100,200, Path.Direction.CW);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                path.op(newPath, Path.Op.XOR);
            }
            canvas.drawPath(path, mPaint);
    
    图片.png

    Op.REVERSE_DIFFERENCE: 从第二条路径中减去第一条路径。

            Path path = new Path();
            path.addCircle(-100,-100,200, Path.Direction.CW);
            Path newPath = new Path();
            newPath.addCircle(100,100,200, Path.Direction.CW);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                path.op(newPath, Path.Op.REVERSE_DIFFERENCE);
            }
            canvas.drawPath(path, mPaint);
    
    图片.png
    (18)reset()rewind()

    将Path清空。

    reset: 不保留内部数据结构,但会保留FillType。
    rewind: 会保留内部的数据结构,但不保留FillType。

    一般使用reset

    (19)toggleInverseFillType

    切换填充规则(即原有规则与反向规则之间相互切换)

    FillType .WINDINGFillType .INVERSE_WINDING相互切换。
    FillType .EVEN_ODDFillType .INVERSE_EVEN_ODD相互切换。

    (20)transform
    transform(Matrix matrix)
    transform(Matrix matrix, Path dst)
    

    对Path进行矩阵操作,我们就拿矩阵的旋转操作来演示,代码如下:

            mPaint.setColor(Color.BLUE);
    
            Path path = new Path();
            
            path.addCircle(100,100,200, Path.Direction.CW);
            path.addCircle(-100,-100,200, Path.Direction.CW);
    
            Matrix matrix = new Matrix();
            matrix.setRotate(degrees);
            path.transform(matrix);
    
            canvas.drawPath(path, mPaint);
    
            degrees = degrees + 2;
    
            invalidate();
    
    54.gif

    另外,第二个方法有个参数dst,意思就是:Path在矩阵操作之后,在Path保存到dst对象。

    (21)rMoveTorLineTo
    • moveTorMoveTo的区别?

    moveTo: 移动的是画笔的位置;
    rMoveTo: 不仅移动画笔的位置,而且直角坐标系也随之移动,此时画笔的位置相当于没有变化。

    • lineTorLineTo的区别?

    moveTo: 移动的是画笔的位置;
    rMoveTo: 不仅移动画笔的位置,而且直角坐标系也随之移动,此时画笔的位置相当于没有变化。

    (22)quadTo、cubicTo、rQuadTo、rCubicTo

    贝赛尔曲线是Path的一个非常重要的知识点,我给它单独整理了一篇文章,如下:

    高级UI<第二十八篇>:贝赛尔曲线

    [本章完...]

    相关文章

      网友评论

        本文标题:高级UI<第二十九篇>:Android开发之Path详解

        本文链接:https://www.haomeiwen.com/subject/iiglactx.html