Android自定义控件探索之旅一7(笔记)

在上一篇文章的末尾提到了Canvas绘制文本的第二种实现方式,是使用了Canvas.drawTextOnPath(...)这个方法,里面会要求传一个Path对象,那么本篇文章主要学习Path相关的内容

Android自定义控件探索之旅一7
Android自定义控件探索之旅一6
Android自定义控件探索之旅一5
Android自定义控件探索之旅一4
Android自定义控件探索之旅一3
Android自定义控件探索之旅一2
Android自定义控件探索之旅一1

Path

首先看下系统源码关于Path的介绍:


/**
 * The Path class encapsulates compound (multiple contour) geometric paths
 * consisting of straight line segments, quadratic curves, and cubic curves.
 * It can be drawn with canvas.drawPath(path, paint), either filled or stroked
 * (based on the paint's Style), or it can be used for clipping or to draw
 * text on a path.
 */

翻译过来就是:
Path这个类封装了由直线、二次曲线和三次曲线组成的复合(多轮廓)几何路径。它可以用Canvas进行绘制。绘制路径(路径,绘制),填充或描边(基于绘制的风格),也可以用于裁剪或绘制路径上的文本。

简单点理解就是:使用Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。

为了兼容性,下面的这张截图去除了部分API21(即安卓版本5.0)以上才添加的方法:


Path常见API

值得一提的是,路径有开放和封闭的区别:


开放和封闭

为了对具体的API体验和使用有原滋原味的体验,下面会把涉及到的API进行源码分析,分析内容含系统源码对方法的解释说明以及参数分析

moveTo、 setLastPoint、 lineTo 和 close
  • lineTo:
 /**
     * Add a line from the last point to the specified point (x,y).
     * If no moveTo() call has been made for this contour, the first point is
     * automatically set to (0,0).
     *
     * @param x The x-coordinate of the end of a line
     * @param y The y-coordinate of the end of a line
     */
    public void lineTo(float x, float y) {
        isSimplePath = false;
        nLineTo(mNativePath, x, y);
    }

这个方法的注释一目了然:从最后的点到指定点(x,y)增加一条线。如果没有对此轮廓线进行 moveTo() 调用,则第一个点将自动设置为(0,0)。由于Path可以用来描述一个图像的轮廓,而图像的轮廓通常都是用一条线构成的。因此这里的某个点就是上次操作结束的点,如果没有进行过操作则默认点为坐标原点。

下面是参考示例代码:

 //创建画笔
Paint mPaint = new Paint();         
//画笔颜色 - 黑色
mPaint.setColor(Color.BLACK);         
//画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE);  
 //边框宽度 - 10   
mPaint.setStrokeWidth(10);             
//移动坐标系到屏幕中心(宽高数据在onSizeChanged中获取)
canvas.translate(mWidth / 2, mHeight / 2); 
//创建Path
Path path = new Path();                 
//lineTo
path.lineTo(200, 200);                      
path.lineTo(200,0);
 //绘制Path
canvas.drawPath(path, mPaint);    
         

运行之后的效果图:


lineTo

代码分析:由于第一次调用lineTo( )之前没有操作,所以默认点就是坐标原点O,结果就是坐标原点O到A(200,200)之间连直线(用蓝色圈1标注);接着第二次调用lineTo,由于上次的结束位置是A(200,200),所以就是A(200,200)到B(200,0)之间的连线(用蓝色圈2标注)。

  • moveTo、 setLastPoint
   /**
     * Set the beginning of the next contour to the point (x,y).
     *
     * @param x The x-coordinate of the start of a new contour
     * @param y The y-coordinate of the start of a new contour
     */
    public void moveTo(float x, float y) {
        nMoveTo(mNativePath, x, y);
    }

    /**
     * Sets the last point of the path.
     *
     * @param dx The new X coordinate for the last point
     * @param dy The new Y coordinate for the last point
     */
    public void setLastPoint(float dx, float dy) {
        isSimplePath = false;
        nSetLastPoint(mNativePath, dx, dy);
    }


由于moveTo以及setLastPoint这2个函数的内部最终调用了Native层的函数,这里就不继续点进源码。首先看下moveTo这个函数,系统源码对其方法的说明:

moveTo(float x, float y):
将下一个轮廓的起始点设置为(x,y),其中x:新轮廓线起始的x坐标,y:新轮廓线起始的y坐标

setLastPoint(float x, float y):
设置路径的最后一点。其中x:最后一点的X坐标,y:最后一点的X坐标

这两个方法虽然在作用上貌似有相似之处(都是设置路径的点),但实际上却是完全不同的作用,具体参照下表:

方法 简介 是否影响之前的操作 是否影响之后操作
moveTo 移动下一次操作的起点位置
setLastPoint 设置之前操作的最后一个点位置

下面是moveTo(float x, float y)参考示例代码:

        Paint mPaint = new Paint();             // 创建画笔
        mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
        mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
        mPaint.setStrokeWidth(10);              // 边框宽度 - 10

        Canvas canvas = new Canvas();
        // 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
        canvas.translate(mWidth / 2 , mHeight / 2);  
        // 创建Path
        Path path = new Path();
        // lineTo
        path.lineTo(200, 200);
        // setLastPoint
        path.moveTo(200,100);
        // lineTo
        path.lineTo(200,0);
        // 绘制Path
        canvas.drawPath(path, mPaint);

moveTo(float x, float y)运行之后的效果图:


moveTo

运行效果及代码逻辑分析:
执行完第一次LineTo的时候,现在的默认点位置变成了A(200,200),但是moveTo将其变成为了C(200,100),因此在第二次调用lineTo的时候就是连接C(200,100) 到 B(200,0) 之间的直线(用蓝色圈2标注)。

下面是setLastPoint(float x, float y)参考示例代码:

        Paint mPaint = new Paint();             // 创建画笔
        mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
        mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
        mPaint.setStrokeWidth(10);              // 边框宽度 - 10

        Canvas canvas = new Canvas();
        // 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
        canvas.translate(mWidth / 2 , mHeight / 2);
        // 创建Path
        Path path = new Path();
        // lineTo
        path.lineTo(200, 200);
        // setLastPoint
        path.setLastPoint(200,100);
        // lineTo
        path.lineTo(200,0);
        // 绘制Path
        canvas.drawPath(path, mPaint);

setLastPoint(float x, float y)运行之后的效果图:


setLastPoint

运行效果及代码逻辑分析:

setLastPoint是重置上一次操作的最后一个点,在执行完第一次的lineTo的时候,最后一个点是A(200,200),而setLastPoint更改最后一个点为C(200,100),所以在实际执行的时候,第一次的lineTo就不是从原点O到A(200,200)的连线了,而变成了从原点O到C(200,100)之间的连线了。
在执行完第一次lineTo和setLastPoint后,最后一个点的位置是C(200,100),所以在第二次调用lineTo的时候就是C(200,100) 到 B(200,0) 之间的连线(用蓝色圈2标注)。

  • close
 /**
     * Close the current contour. If the current point is not equal to the
     * first point of the contour, a line segment is automatically added.
     */
    public void close() {
        isSimplePath = false;
        nClose(mNativePath);
    }

这个方法的英文注释翻译过来就是:关闭当前轮廓线。如果当前点不等于轮廓的第一个点,则自动添加线段。

下面是close参考示例代码:

        Paint mPaint = new Paint();             // 创建画笔
        mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
        mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
        mPaint.setStrokeWidth(10);              // 边框宽度 - 10

        Canvas canvas = new Canvas();
        
        // 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
        canvas.translate(mWidth / 2 , mHeight / 2);
        // 创建Path
        Path path = new Path();

        // lineTo
        path.lineTo(200,0);

        // lineTo
        path.lineTo(200, 200);

        // lineTo
        path.lineTo(200,0);

        // close
        path.close();

        // 绘制Path
        canvas.drawPath(path, mPaint);

close运行之后的效果图:


close

运行效果及代码逻辑分析:
两个lineTo分别代表第1和第2条线,而close在此处的作用就算连接了B(200,0)点和原点O之间的第3条线,使之形成一个封闭的图形。需要注意的是,close的作用是封闭路径,但是与连接当前最后一个点和第一个点并不等价。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做(这样也就失去了意义)

addXxx与arcTo

Path中的addXxx主要是在Path中添加基本图形,既然是基本图形,那也可以分为几类:

第一类(基本形状)
// 第一类(基本形状)

// 圆形
public void addCircle (float x, float y, float radius, Path.Direction dir)
// 椭圆
public void addOval (RectF oval, Path.Direction dir)
// 矩形
public void addRect (float left, float top, float right, float bottom, Path.Direction dir)
public void addRect (RectF rect, Path.Direction dir)
// 圆角矩形
public void addRoundRect (RectF rect, float[] radii, Path.Direction dir)

public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)

第一类(基本形状)里面具体的API,方法中最后都有一个 Path.Direction,这个Path.Direction按照老规矩首先看下源码:

    /**
     * Specifies how closed shapes (e.g. rects, ovals) are oriented when they
     * are added to a path.
     */
    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;
    }

这个方法的注释翻译过来就是:指定添加到路径时封闭形状(例如矩形、椭圆)的方向。这里的clockwise ,是顺时针的意思,也就是 CW (0);counter-clockwise是逆时针的意思,也就是 CW (1)

第二类(Path)

    /**
     * Add a copy of src to the path
     *
     * @param src The path that is appended to the current path
     */
    public void addPath(Path src) {
        isSimplePath = false;
        nAddPath(mNativePath, src.mNativePath);
    }

    /**
     * Add a copy of src to the path, transformed by matrix
     *
     * @param src The path to add as a new contour
     */
    public void addPath(Path src, Matrix matrix) {
        if (!src.isSimplePath) isSimplePath = false;
        nAddPath(mNativePath, src.mNativePath, matrix.native_instance);
    }

   /**
     * Add a copy of src to the path, offset by (dx,dy)
     *
     * @param src The path to add as a new contour
     * @param dx  The amount to translate the path in X as it is added
     */
    public void addPath(Path src, float dx, float dy) {
        isSimplePath = false;
        nAddPath(mNativePath, src.mNativePath, dx, dy);
    }

可以看到这是一个方法重载,这个方法的注释翻译过来就是(对应顺序):

  • 将src的副本添加到路径中
  • 将src的副本添加到路径中,由Matrix(矩阵)变换
  • 将src的副本添加到路径中,偏移量为(dx,dy)

总之,这类方式就是将两个Path合并成为一个。然后可以设置矩阵、设置偏移量来进行自定义数据的设置。

下面是drawPath()参考示例代码:


        Paint mPaint = new Paint();             // 创建画笔
        mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
        mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
        mPaint.setStrokeWidth(10);              // 边框宽度 - 10

        Canvas canvas = new Canvas();

        // 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
        canvas.translate(mWidth / 2, mHeight / 2);

        // <-- 注意 翻转y坐标轴
        canvas.scale(1,-1);

        Path path = new Path();
        Path src = new Path();
        //画矩形
        path.addRect(-200,-200,200,200, Path.Direction.CW);
        //画圆形
        src.addCircle(0,0,100, Path.Direction.CW);

        path.addPath(src,0,200);

        // 绘制合并后的路径
        mPaint.setColor(Color.BLACK);

        canvas.drawPath(path,mPaint);

addPath()运行之后的效果图:


addPath

运行效果及代码逻辑分析:
两个Path(矩形和圆形)中心都是坐标原点,在将包含圆形的path添加到包含矩形的path之前将其进行移动了一段距离,最终绘制出来的效果就如上面所示

第三类(addArc与arcTo)

老规矩,先看下官方系统的源码

   /**
     * Add the specified arc to the path as a new contour.
     *
     * @param oval The bounds of oval defining the shape and size of the arc
     * @param startAngle Starting angle (in degrees) where the arc begins
     * @param sweepAngle Sweep angle (in degrees) measured clockwise
     */
    public void addArc(RectF oval, float startAngle, float sweepAngle) {
        addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
    }

    /**
     * Add the specified arc to the path as a new contour.
     *
     * @param startAngle Starting angle (in degrees) where the arc begins
     * @param sweepAngle Sweep angle (in degrees) measured clockwise
     */
    public void addArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle) {
        isSimplePath = false;
        nAddArc(mNativePath, left, top, right, bottom, startAngle, sweepAngle);
    }


    /**
     * Append the specified arc to the path as a new contour. If the start of
     * the path is different from the path's current last point, then an
     * automatic lineTo() is added to connect the current contour to the
     * start of the arc. However, if the path is empty, then we call moveTo()
     * with the first point of the arc.
     *
     * @param oval        The bounds of oval defining shape and size of the arc
     * @param startAngle  Starting angle (in degrees) where the arc begins
     * @param sweepAngle  Sweep angle (in degrees) measured clockwise
     */
    public void arcTo(RectF oval, float startAngle, float sweepAngle) {
        arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
    }


    /**
     * Append the specified arc to the path as a new contour. If the start of
     * the path is different from the path's current last point, then an
     * automatic lineTo() is added to connect the current contour to the
     * start of the arc. However, if the path is empty, then we call moveTo()
     * with the first point of the arc.
     *
     * @param oval        The bounds of oval defining shape and size of the arc
     * @param startAngle  Starting angle (in degrees) where the arc begins
     * @param sweepAngle  Sweep angle (in degrees) measured clockwise, treated
     *                    mod 360.
     * @param forceMoveTo If true, always begin a new contour with the arc
     */
    public void arcTo(RectF oval, float startAngle, float sweepAngle,
                      boolean forceMoveTo) {
        arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
    }


    /**
     * Append the specified arc to the path as a new contour. If the start of
     * the path is different from the path's current last point, then an
     * automatic lineTo() is added to connect the current contour to the
     * start of the arc. However, if the path is empty, then we call moveTo()
     * with the first point of the arc.
     *
     * @param startAngle  Starting angle (in degrees) where the arc begins
     * @param sweepAngle  Sweep angle (in degrees) measured clockwise, treated
     *                    mod 360.
     * @param forceMoveTo If true, always begin a new contour with the arc
     */
    public void arcTo(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean forceMoveTo) {
        isSimplePath = false;
        nArcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
    }


addArcarcTo这2个方法都是方法重载,下面就逐一分析:

addArc 这个方法的注释翻译过来就是:将指定的弧作为新的轮廓添加到路径中。

  • 参数 RectF oval :椭圆的边界,定义弧的形状和大小
  • 参数 float startAngle:圆弧开始的起始角(度)
  • 参数 float sweepAngle:顺时针测量的角度

arcTo 这个方法的注释翻译过来就是:将指定的弧作为新轮廓追加到路径。如果路径的起始点与当前路径的最后一点不同,则添加一个自动lineTo()将当前轮廓线连接到圆弧的起始点。但是,如果路径是空的,则使用弧线的第一点调用moveTo()

  • 参数 RectF oval :确定圆弧形状和大小的椭圆边界
  • 参数 float startAngle :圆弧开始的起始角(度)
  • 参数 float sweepAngle :顺时针测量的角度
  • 参数 boolean forceMoveTo :如果为True,总是以圆弧开始新的轮廓线;如果为False,则不移动,而是连接最后一个点与圆弧起点

值得一提的是:sweepAngle取值范围是 [-360, 360),不包括360。当 >= 360 或者 < -360 时将不会绘制任何内容, 对于360,你可以用一个接近的值替代,例如: 359.99。

isEmpty(返回值 boolean)
isEmpty()这个方法主要是判断path中是否包含内容,如果没有内容,则为true,有内容则为false

isRect(返回值 boolean)
isRect()这个方法顾名思义,判断path是否是一个矩形,如果是一个矩形的话,会将矩形的信息存放进参数rect中

set(返回值 void)
set(Path path),这个方法是将新的path赋值到现有path中

offset
offset(float dx, float dy)这个方法就是对path进行一段平移,它和Canvas中的translate作用很像,但Canvas作用于整个画布,而path的offset只作用于当前path。

如果这篇文章对您有开发or学习上的些许帮助,希望各位看官留下宝贵的star,谢谢。

Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容