精华内容
参与话题
问答
  • 安卓自定义View进阶-Canvas之画布操作

    万次阅读 多人点赞 2016-01-28 12:54:14
    上一篇【Canvas 之绘制基本形状】中我们了解了如何使用 Canvas 绘制基本图形,本次了解一些基本的画布操作。合理的使用画布操作可以帮助你用更容易理解的方式创作想要的效果。

    Canvas之画布操作

    作者微博: @GcsSloop

    【本系列相关文章】

    上一篇Canvas之绘制基本形状中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。

    本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。

    一.Canvas的常用操作速查表

    操作类型 相关API 备注
    绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布
    绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
    绘制图片 drawBitmap, drawPicture 绘制位图和图片
    绘制文本 drawText, drawPosText, drawTextOnPath 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
    绘制路径 drawPath 绘制路径,绘制贝塞尔曲线时也需要用到该函数
    顶点操作 drawVertices, drawBitmapMesh 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用
    画布剪裁 clipPath, clipRect 设置画布的显示区域
    画布快照 save, restore, saveLayerXxx, restoreToCount, getSaveCount 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
    画布变换 translate, scale, rotate, skew 依次为 位移、缩放、 旋转、错切
    Matrix(矩阵) getMatrix, setMatrix, concat 实际上画布的位移,缩放等操作的都是图像矩阵Matrix, 只不过Matrix比较难以理解和使用,故封装了一些常用的方法。

    二.Canvas基本操作

    1.画布操作

    为什么要有画布操作?

    画布操作可以帮助我们用更加容易理解的方式制作图形。

    例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?

    按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。

    然而这是否是被固有思维禁锢了?

    假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

    合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

    PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。


    ⑴位移(translate)

    translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。
    请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动,如下:

            // 省略了创建画笔的代码
    
            // 在坐标原点绘制一个黑色圆形
            mPaint.setColor(Color.BLACK);
            canvas.translate(200,200);
            canvas.drawCircle(0,0,100,mPaint);
    
            // 在坐标原点绘制一个蓝色圆形
            mPaint.setColor(Color.BLUE);
            canvas.translate(200,200);
            canvas.drawCircle(0,0,100,mPaint);

    005Xtdi2jw1f2f1ph46qaj30u01hcgm3.jpg

    我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的


    ⑵缩放(scale)

    缩放提供了两个方法,如下:

     public void scale (float sx, float sy)
    
     public final void scale (float sx, float sy, float px, float py)

    这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。

    缩放比例(sx,sy)取值范围详解:

    取值范围(n) 说明
    [-∞, -1) 先根据缩放中心放大n倍,再根据中心轴进行翻转
    -1 根据缩放中心轴进行翻转
    (-1, 0) 先根据缩放中心缩小到n,再根据中心轴进行翻转
    0 不会显示,若sx为0,则宽度为0,不会显示,sy同理
    (0, 1) 根据缩放中心缩小到n
    1 没有变化
    (1, +∞) 根据缩放中心放大n倍

    如果在缩放时稍微注意一下就会发现缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴,如下:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.scale(0.5f,0.5f);                // 画布缩放
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    (为了更加直观,我添加了一个坐标系,可以比较明显的看出,缩放中心就是坐标原点)

    005Xtdi2jw1f2f1vphdjjj30u01hct9r.jpg

    接下来我们使用第二种方法让缩放中心位置稍微改变一下,如下:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.scale(0.5f,0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    (图中用箭头指示的就是缩放中心。)

    005Xtdi2jw1f2f1w7kv8dj30u01hct9s.jpg

    前面两个示例缩放的数值都是正数,按照表格中的说明,当缩放比例为负数的时候会根据缩放中心轴进行翻转,下面我们就来实验一下:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
    
            canvas.scale(-0.5f,-0.5f);          // 画布缩放
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    005Xtdi2jw1f2f1x76o6qj30u01hc0tu.jpg

    为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的。

    由于本次未对缩放中心进行偏移,所有默认的缩放中心就是坐标原点,中心轴就是x轴和y轴。

    本次缩放可以看做是先根据缩放中心(坐标原点)缩放到原来的0.5倍,然后分别按照x轴和y轴进行翻转。

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
    
            canvas.scale(-0.5f,-0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    005Xtdi2jw1f2f1xth4p6j30u01hc0u4.jpg

    添加了这么多的辅助内容,希望大家能够看懂。

    本次对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了。

    PS:和位移(translate)一样,缩放也是可以叠加的。

       canvas.scale(0.5f,0.5f);
       canvas.scale(0.5f,0.1f);

    调用两次缩放则 x轴实际缩放为0.5x0.5=0.25 y轴实际缩放为0.5x0.1=0.05

    下面我们利用这一特性制作一个有趣的图形。

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(-400,-400,400,400);   // 矩形区域
    
            for (int i=0; i<=20; i++)
            {
                canvas.scale(0.9f,0.9f);
                canvas.drawRect(rect,mPaint);
            }

    005Xtdi2jw1f2f1yfn22xj30u01hcta9.jpg


    ⑶旋转(rotate)

    旋转提供了两种方法:

      public void rotate (float degrees)
    
      public final void rotate (float degrees, float px, float py)

    和缩放一样,第二种方法多出来的两个参数依旧是控制旋转中心点的。

    默认的旋转中心依旧是坐标原点:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.rotate(180);                     // 旋转180度 <-- 默认旋转中心为原点
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    005Xtdi2jw1f2f1yws38nj30u01hcmy8.jpg

    改变旋转中心位置:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.rotate(180,200,0);               // 旋转180度 <-- 旋转中心向右偏移200个单位
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    005Xtdi2jw1f2f1zcmwb2j30u01hcmy9.jpg

    好吧,旋转也是可叠加的

         canvas.rotate(180);
         canvas.rotate(20);

    调用两次旋转,则实际的旋转角度为180+20=200度。

    为了演示这一个效果,我做了一个不明觉厉的东西:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            canvas.drawCircle(0,0,400,mPaint);          // 绘制两个圆形
            canvas.drawCircle(0,0,380,mPaint);
    
            for (int i=0; i<=360; i+=10){               // 绘制圆形之间的连接线
                canvas.drawLine(0,380,0,400,mPaint);
                canvas.rotate(10);
            }

    005Xtdi2jw1f2f1zsnj00j30u01hc75a.jpg


    ⑷错切(skew)

    skew这里翻译为错切,错切是特殊类型的线性变换。

    错切只提供了一种方法:

      public void skew (float sx, float sy)

    参数含义:

    float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,

    float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.

    变换后:

    X = x + sx * y
    Y = sy * x + y

    示例:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,0,200,200);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.skew(1,0);                       // 水平错切 <- 45度
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    005Xtdi2jw1f2f20h7i23j30u01hcdgq.jpg

    如你所想,错切也是可叠加的,不过请注意,调用次序不同绘制结果也会不同

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,0,200,200);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.skew(1,0);                       // 水平错切
            canvas.skew(0,1);                       // 垂直错切
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    005Xtdi2jw1f2f20w0rffj30u01hcgm8.jpg


    ⑸快照(save)和回滚(restore)


    Q: 为什存在快照与回滚

    A:画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。

    与之相关的API:

    相关API 简介
    save 把当前的画布的状态进行保存,然后放入特定的栈中
    saveLayerXxx 新建一个图层,并放入特定的栈中
    restore 把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布
    restoreToCount 弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复
    getSaveCount 获取栈中内容的数量(即保存次数)

    下面对其中的一些概念和方法进行分析:

    状态栈:

    其实这个栈我也不知道叫什么名字,暂时叫做状态栈吧,它看起来像下面这样:

    这个栈可以存储画布状态和图层状态。

    Q:什么是画布和图层?

    A:实际上我们看到的画布是由多个图层构成的,如下图(图片来自网络):

    实际上我们之前讲解的绘制操作和画布操作都是在默认图层上进行的。

    在通常情况下,使用默认图层就可满足需求,但是如果需要绘制比较复杂的内容,如地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)等,则分图层绘制比较好一些。

    你可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。

    SaveFlags
    数据类型 名称 简介
    int ALL_SAVE_FLAG 默认,保存全部状态
    int CLIP_SAVE_FLAG 保存剪辑区
    int CLIP_TO_LAYER_SAVE_FLAG 剪裁区作为图层保存
    int FULL_COLOR_LAYER_SAVE_FLAG 保存图层的全部色彩通道
    int HAS_ALPHA_LAYER_SAVE_FLAG 保存图层的alpha(不透明度)通道
    int MATRIX_SAVE_FLAG 保存Matrix信息(translate, rotate, scale, skew)
    save

    save 有两种方法:

      // 保存全部状态
      public int save ()
    
      // 根据saveFlags参数保存一部分状态
      public int save (int saveFlags)

    可以看到第二种方法比第一种多了一个saveFlags参数,使用这个参数可以只保存一部分状态,更加灵活,这个saveFlags参数具体可参考上面表格中的内容。

    每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。

    saveLayerXxx

    saveLayerXxx有比较多的方法:

    // 无图层alpha(不透明度)通道
    public int saveLayer (RectF bounds, Paint paint)
    public int saveLayer (RectF bounds, Paint paint, int saveFlags)
    public int saveLayer (float left, float top, float right, float bottom, Paint paint)
    public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)
    
    // 有图层alpha(不透明度)通道
    public int saveLayerAlpha (RectF bounds, int alpha)
    public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
    public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
    public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)

    注意:saveLayerXxx方法会让你花费更多的时间去渲染图像(图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。

    使用saveLayerXxx方法,也会将图层状态也放入状态栈中,同样使用restore方法进行恢复。

    这个暂时不过多讲述,如果以后用到详细讲解。(因为这里面东西也有不少啊QAQ)

    restore

    状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。

    同样以上面状态栈图片为例,调用一次restore方法则将状态栈中第5次取出,根据里面保存的状态进行状态恢复。

    restoreToCount

    弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。

    以上面状态栈图片为例,如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。

    getSaveCount

    获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。

    不过请注意,该函数的最小返回值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。

    常用格式

    虽然关于状态的保存和回滚啰嗦了不少,不过大多数情况下只需要记住下面的步骤就可以了:

       save();      //保存状态
       ...          //具体操作
       restore();   //回滚到之前的状态

    这种方式也是最简单和最容易理解的使用方法。


    三.总结

    如本文一开始所说,合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果。

    (,,• ₃ •,,)

    PS: 由于本人英文水平有限,某些地方可能存在误解或词语翻译不准确,如果你对此有疑问可以提交Issues进行反馈。

    About Me

    作者微博: @GcsSloop

    005Xtdi2gw1f1qn89ihu3j315o0dwwjc.jpg


    四.参考资料

    Canvas

    canvas变换与操作

    Canvas之translate、scale、rotate、skew方法讲解

    Canvas的save(),saveLayer()和restore()浅谈

    Graphics->Layers




    展开全文
  • 上一篇Canvas之绘制基本图形中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。一...

    上一篇Canvas之绘制基本图形中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。

    本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。

    一.Canvas的常用操作速查表

    操作类型 相关API 备注
    绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布
    绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
    绘制图片 drawBitmap, drawPicture 绘制位图和图片
    绘制文本 drawText, drawPosText, drawTextOnPath 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
    绘制路径 drawPath 绘制路径,绘制贝塞尔曲线时也需要用到该函数
    顶点操作 drawVertices, drawBitmapMesh 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用
    画布剪裁 clipPath, clipRect 设置画布的显示区域
    画布快照 save, restore, saveLayerXxx, restoreToCount, getSaveCount 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
    画布变换 translate, scale, rotate, skew 依次为 位移、缩放、 旋转、错切
    Matrix(矩阵) getMatrix, setMatrix, concat 实际上画布的位移,缩放等操作的都是图像矩阵Matrix, 只不过Matrix比较难以理解和使用,故封装了一些常用的方法。

    二.Canvas基本操作

    1.画布操作

    为什么要有画布操作?

    画布操作可以帮助我们用更加容易理解的方式制作图形。

    例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?

    按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。

    然而这是否是被固有思维禁锢了?

    假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

    合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

    PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。


    ⑴位移(translate)

    translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动,如下:

    // 省略了创建画笔的代码
    
    // 在坐标原点绘制一个黑色圆形
    mPaint.setColor(Color.BLACK);
    canvas.translate(200,200);
    canvas.drawCircle(0,0,100,mPaint);
    
    // 在坐标原点绘制一个蓝色圆形
    mPaint.setColor(Color.BLUE);
    canvas.translate(200,200);
    canvas.drawCircle(0,0,100,mPaint);
    

    我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的


    ⑵缩放(scale)

    缩放提供了两个方法,如下:

    public void scale (float sx, float sy)
    
    public final void scale (float sx, float sy, float px, float py)
    

    这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。

    缩放比例(sx,sy)取值范围详解:

    取值范围(n) 说明
    [-∞, -1) 先根据缩放中心放大n倍,再根据中心轴进行翻转
    -1 根据缩放中心轴进行翻转
    (-1, 0) 先根据缩放中心缩小到n,再根据中心轴进行翻转
    0 不会显示,若sx为0,则宽度为0,不会显示,sy同理
    (0, 1) 根据缩放中心缩小到n
    1 没有变化
    (1, +∞) 根据缩放中心放大n倍

    如果在缩放时稍微注意一下就会发现缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴,如下:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.scale(0.5f,0.5f);                // 画布缩放
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    (为了更加直观,我添加了一个坐标系,可以比较明显的看出,缩放中心就是坐标原点)

    接下来我们使用第二种方法让缩放中心位置稍微改变一下,如下:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.scale(0.5f,0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    (图中用箭头指示的就是缩放中心。)

    前面两个示例缩放的数值都是正数,按照表格中的说明,当缩放比例为负数的时候会根据缩放中心轴进行翻转,下面我们就来实验一下:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    
    canvas.scale(-0.5f,-0.5f);          // 画布缩放
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的。

    由于本次未对缩放中心进行偏移,所有默认的缩放中心就是坐标原点,中心轴就是x轴和y轴。

    本次缩放可以看做是先根据缩放中心(坐标原点)缩放到原来的0.5倍,然后分别按照x轴和y轴进行翻转。

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.scale(-0.5f,-0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    添加了这么多的辅助内容,希望大家能够看懂。

    本次对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了。

    PS:和位移(translate)一样,缩放也是可以叠加的。

    canvas.scale(0.5f,0.5f);
    canvas.scale(0.5f,0.1f);
    

    调用两次缩放则 x轴实际缩放为0.5x0.5=0.25 y轴实际缩放为0.5x0.1=0.05

    下面我们利用这一特性制作一个有趣的图形。

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(-400,-400,400,400);   // 矩形区域
    
    for (int i=0; i<=20; i++)
    {
        canvas.scale(0.9f,0.9f);
        canvas.drawRect(rect,mPaint);
    }
    


    ⑶旋转(rotate)

    旋转提供了两种方法:

    public void rotate (float degrees)
    
    public final void rotate (float degrees, float px, float py)
    

    和缩放一样,第二种方法多出来的两个参数依旧是控制旋转中心点的。

    默认的旋转中心依旧是坐标原点:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.rotate(180);                     // 旋转180度 <-- 默认旋转中心为原点
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    改变旋转中心位置:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.rotate(180,200,0);               // 旋转180度 <-- 旋转中心向右偏移200个单位
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    好吧,旋转也是可叠加的

    canvas.rotate(180);
    canvas.rotate(20);
    

    调用两次旋转,则实际的旋转角度为180+20=200度。

    为了演示这一个效果,我做了一个不明觉厉的东西:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    canvas.drawCircle(0,0,400,mPaint);          // 绘制两个圆形
    canvas.drawCircle(0,0,380,mPaint);
    
    for (int i=0; i<=360; i+=10){               // 绘制圆形之间的连接线
       canvas.drawLine(0,380,0,400,mPaint);
       canvas.rotate(10);
    }
    


    ⑷错切(skew)

    skew这里翻译为错切,错切是特殊类型的线性变换。

    错切只提供了一种方法:

    public void skew (float sx, float sy)
    

    参数含义:
    float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
    float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.

    变换后:

    X = x + sx * y
    Y = sy * x + y
    

    示例:

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,0,200,200);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.skew(1,0);                       // 水平错切 <- 45度
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    

    如你所想,错切也是可叠加的,不过请注意,调用次序不同绘制结果也会不同

    // 将坐标系原点移动到画布正中心
    canvas.translate(mWidth / 2, mHeight / 2);
    
    RectF rect = new RectF(0,0,200,200);   // 矩形区域
    
    mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
    canvas.drawRect(rect,mPaint);
    
    canvas.skew(1,0);                       // 水平错切
    canvas.skew(0,1);                       // 垂直错切
    
    mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
    canvas.drawRect(rect,mPaint);
    


    ⑸快照(save)和回滚(restore)

    Q: 为什存在快照与回滚
    A:画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。

    与之相关的API:

    相关API 简介
    save 把当前的画布的状态进行保存,然后放入特定的栈中
    saveLayerXxx 新建一个图层,并放入特定的栈中
    restore 把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布
    restoreToCount 弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复
    getSaveCount 获取栈中内容的数量(即保存次数)

    下面对其中的一些概念和方法进行分析:

    状态栈:

    其实这个栈我也不知道叫什么名字,暂时叫做状态栈吧,它看起来像下面这样:

    这个栈可以存储画布状态和图层状态。

    Q:什么是画布和图层?
    A:实际上我们看到的画布是由多个图层构成的,如下图(图片来自网络):

    实际上我们之前讲解的绘制操作和画布操作都是在默认图层上进行的。
    在通常情况下,使用默认图层就可满足需求,但是如果需要绘制比较复杂的内容,如地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)等,则分图层绘制比较好一些。
    你可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。

    SaveFlags
    名称 简介
    ALL_SAVE_FLAG 默认,保存全部状态
    CLIP_SAVE_FLAG 保存剪辑区
    CLIP_TO_LAYER_SAVE_FLAG 剪裁区作为图层保存
    FULL_COLOR_LAYER_SAVE_FLAG 保存图层的全部色彩通道
    HAS_ALPHA_LAYER_SAVE_FLAG 保存图层的alpha(不透明度)通道
    MATRIX_SAVE_FLAG 保存Matrix信息( translate, rotate, scale, skew)
    save

    save 有两种方法:

    // 保存全部状态
    public int save ()
    
    // 根据saveFlags参数保存一部分状态
    public int save (int saveFlags)
    

    可以看到第二种方法比第一种多了一个saveFlags参数,使用这个参数可以只保存一部分状态,更加灵活,这个saveFlags参数具体可参考上面表格中的内容。

    每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。

    saveLayerXxx

    saveLayerXxx有比较多的方法:

    // 无图层alpha(不透明度)通道
    public int saveLayer (RectF bounds, Paint paint)
    public int saveLayer (RectF bounds, Paint paint, int saveFlags)
    public int saveLayer (float left, float top, float right, float bottom, Paint paint)
    public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)
    
    // 有图层alpha(不透明度)通道
    public int saveLayerAlpha (RectF bounds, int alpha)
    public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
    public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
    public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)
    

    注意:saveLayerXxx方法会让你花费更多的时间去渲染图像(图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。

    使用saveLayerXxx方法,也会将图层状态也放入状态栈中,同样使用restore方法进行恢复。

    这个暂时不过多讲述,如果以后用到详细讲解。(因为这里面东西也有不少啊QAQ)

    restore

    状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。

    同样以上面状态栈图片为例,调用一次restore方法则将状态栈中第5次取出,根据里面保存的状态进行状态恢复。

    restoreToCount

    弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。

    以上面状态栈图片为例,如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。

    getSaveCount

    获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。

    不过请注意,该函数的最小返回值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。

    常用格式

    虽然关于状态的保存和回滚啰嗦了不少,不过大多数情况下只需要记住下面的步骤就可以了:

    save();      //保存状态
    ...          //具体操作
    restore();   //回滚到之前的状态
    

    这种方式也是最简单和最容易理解的使用方法。


    三.总结

    如本文一开始所说,合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果。

    (,,• ₃ •,,)

    PS: 由于本人英文水平有限,某些地方可能存在误解或词语翻译不准确,如果你对此有疑问可以提交Issues进行反馈。

    About

    本系列相关文章

    作者微博: GcsSloop


    四.参考资料

    Canvas
    canvas变换与操作
    Canvas之translate、scale、rotate、skew方法讲解
    Canvas的save(),saveLayer()和restore()浅谈
    Graphics->Layers

    <hr>
    

    展开全文
  • 上一节讲到了如何用画布绘制基本的图形,但是很多的图形绘制也离不开对画布操作,比如图形的位移,缩放,错切,图层的保存,回滚等,使用我们的Canvas自带的一些api可以为我们减少很多复杂的数学运算。大大减少...

           上一节讲到了如何用画布绘制基本的图形,但是很多的图形绘制也离不开对画布的操作,比如图形的位移,缩放,错切,图层的保存,回滚等,使用我们的Canvas自带的一些api可以为我们减少很多复杂的数学运算。大大减少我们的开发复杂度,否则数学函数太复杂了。

    一、Canvas的常用操作api

        系统为我们提供了一些列的api接口,供我们使用

       官方详细地址点击打开链接

    二、画布的基本操作

    1.画布操作

    为什么要有画布操作?

    画布操作可以帮助我们用更加容易理解的方式制作图形。

    例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?

    按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。

    然而这是否是被固有思维禁锢了?

    假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

    合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

    PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。

    位移(translate)

    translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,根据上一次位移的坐标点累加。而不是每次基于屏幕左上角的(0,0)点移动,如下:

     //画布的原点首先移动到200,200.
            canvas.translate(200,200);
            mPaint.setColor(Color.BLUE);
            canvas.drawCircle(0,0,100,mPaint);
    
            //然后在200,200,的基础上再次移动200,200
            mPaint.setColor(Color.BLACK);
            canvas.translate(200,200);
            canvas.drawCircle(0,0,100,mPaint);

    ⑵缩放(scale)

    缩放提供了两个方法,如下:这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。

     /**
         * Preconcat the current matrix with the specified scale.
         *
         * @param sx The amount to scale in X
         * @param sy The amount to scale in Y
         */
        public void scale(float sx, float sy) {
            native_scale(mNativeCanvasWrapper, sx, sy);
        }
    
        /**
         * Preconcat the current matrix with the specified scale.
         *
         * @param sx The amount to scale in X
         * @param sy The amount to scale in Y
         * @param px The x-coord for the pivot point (unchanged by the scale)
         * @param py The y-coord for the pivot point (unchanged by the scale)
         */
        public final void scale(float sx, float sy, float px, float py) {
            translate(px, py);
            scale(sx, sy);
            translate(-px, -py);
        }

    当参数为正数的时候看效果

    //移动画布的原点为屏幕中心点
            canvas.translate(width/2,height/2);
            //绘画出两条坐标轴
            mPaint.setColor(Color.RED);
            canvas.drawLine(-500,0,500,0,mPaint);
            canvas.drawLine(0,-800,0,800,mPaint);
            //画出矩形
            RectF rectF = new RectF(0,-400,400,0);
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(2);
            canvas.drawRect(rectF,mPaint);
            canvas.scale(0.5f,0.5f);
            canvas.drawRect(rectF,mPaint);


    当移动缩放原点到200,0的时候

     //画出矩形
            RectF rectF = new RectF(0,-400,400,0);
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(2);
            canvas.drawRect(rectF,mPaint);
            canvas.scale(0.5f,0.5f,200,0);
            canvas.drawRect(rectF,mPaint);

    当设置缩放的参数为负数的时候,代表首先缩放,然后翻转,如果x是负数,那么就是根据Y轴翻转。如果y是负数,那么就是根据X轴旋转。同时未负数代表,x轴和y轴同时都旋转

    //画出矩形
            RectF rectF = new RectF(0,-400,400,0);
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(2);
            canvas.drawRect(rectF,mPaint);
            canvas.scale(-0.5f,-0.5f,200,0);
            canvas.drawRect(rectF,mPaint);

    缩放还可以进行累加,来一个小的例子看看

     RectF rectF = new RectF(0,-400,400,0);
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(2);
            canvas.drawRect(rectF,mPaint);
            canvas.scale(-0.5f,-0.5f,200,0);
            canvas.drawRect(rectF,mPaint);
            //画布的缩放是可以叠加累计的
            for (int x =0; x<20;x++){
                canvas.scale(0.9f,0.9f);
                canvas.drawRect(rectF,mPaint);
            }


    ⑶旋转(rotate)

    旋转提供了两种方法:和缩放一样,第二种方法多出来的两个参数依旧是控制旋转中心点的。默认的旋转中心依旧是坐标原点:

    public void rotate (float degrees)
      
    public final void rotate (float degrees, float px, float py)

    默认坐标原点旋转

      //画出矩形
            RectF rectF = new RectF(0,-400,400,0);
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(2);
            canvas.drawRect(rectF,mPaint);
            //旋转
            mPaint.setColor(Color.BLUE);
            //旋转180°,改变旋转的中心
            canvas.rotate(180);
            canvas.drawRect(rectF,mPaint);


    第二种,修改旋转的坐标原点

     //旋转
            mPaint.setColor(Color.BLUE);
            //旋转180°,改变旋转的中心
            canvas.rotate(180,200,0);
            canvas.drawRect(rectF,mPaint);

    旋转也是可以累加的,每次旋转之后的角度,等于在前一次的基础上相加。来一个例子看一下

    //绘画出两条坐标轴
            mPaint.setColor(Color.RED);
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawLine(-500,0,500,0,mPaint);
            canvas.drawLine(0,-800,0,800,mPaint);
    
            //绘制两个圆
            canvas.drawCircle(0,0,400,mPaint);
            canvas.drawCircle(0,0,380,mPaint);
    
            for (int x =0;x< 360;x+=10){
                canvas.drawLine(0,380,0,400,mPaint);
                canvas.rotate(10);
            }



    Canvas之画布操作

    作者微博:@GcsSloop

    【本系列相关文章】

    上一篇Canvas之绘制基本形状中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。

    本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。

    一.Canvas的常用操作速查表

    操作类型 相关API 备注
    绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布
    绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
    绘制图片 drawBitmap, drawPicture 绘制位图和图片
    绘制文本 drawText, drawPosText, drawTextOnPath 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
    绘制路径 drawPath 绘制路径,绘制贝塞尔曲线时也需要用到该函数
    顶点操作 drawVertices, drawBitmapMesh 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用
    画布剪裁 clipPath, clipRect 设置画布的显示区域
    画布快照 save, restore, saveLayerXxx, restoreToCount, getSaveCount 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
    画布变换 translate, scale, rotate, skew 依次为 位移、缩放、 旋转、错切
    Matrix(矩阵) getMatrix, setMatrix, concat 实际上画布的位移,缩放等操作的都是图像矩阵Matrix, 只不过Matrix比较难以理解和使用,故封装了一些常用的方法。

    二.Canvas基本操作

    1.画布操作

    为什么要有画布操作?

    画布操作可以帮助我们用更加容易理解的方式制作图形。

    例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?

    按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。

    然而这是否是被固有思维禁锢了?

    假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

    合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

    PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。


    ⑴位移(translate)

    translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动,如下:

            // 省略了创建画笔的代码
            
            // 在坐标原点绘制一个黑色圆形
            mPaint.setColor(Color.BLACK);
            canvas.translate(200,200);
            canvas.drawCircle(0,0,100,mPaint);
    
            // 在坐标原点绘制一个蓝色圆形
            mPaint.setColor(Color.BLUE);
            canvas.translate(200,200);
            canvas.drawCircle(0,0,100,mPaint);

    我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的


    ⑵缩放(scale)

    缩放提供了两个方法,如下:

     public void scale (float sx, float sy)
    
     public final void scale (float sx, float sy, float px, float py)

    这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。

    缩放比例(sx,sy)取值范围详解:

    取值范围(n) 说明
    [-∞, -1) 先根据缩放中心放大n倍,再根据中心轴进行翻转
    -1 根据缩放中心轴进行翻转
    (-1, 0) 先根据缩放中心缩小到n,再根据中心轴进行翻转
    0 不会显示,若sx为0,则宽度为0,不会显示,sy同理
    (0, 1) 根据缩放中心缩小到n
    1 没有变化
    (1, +∞) 根据缩放中心放大n倍

    如果在缩放时稍微注意一下就会发现缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴,如下:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.scale(0.5f,0.5f);                // 画布缩放
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    (为了更加直观,我添加了一个坐标系,可以比较明显的看出,缩放中心就是坐标原点)

    接下来我们使用第二种方法让缩放中心位置稍微改变一下,如下:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.scale(0.5f,0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    (图中用箭头指示的就是缩放中心。)

    前面两个示例缩放的数值都是正数,按照表格中的说明,当缩放比例为负数的时候会根据缩放中心轴进行翻转,下面我们就来实验一下:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
    
            canvas.scale(-0.5f,-0.5f);          // 画布缩放
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的。

    由于本次未对缩放中心进行偏移,所有默认的缩放中心就是坐标原点,中心轴就是x轴和y轴。

    本次缩放可以看做是先根据缩放中心(坐标原点)缩放到原来的0.5倍,然后分别按照x轴和y轴进行翻转。

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
    
            canvas.scale(-0.5f,-0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    添加了这么多的辅助内容,希望大家能够看懂。

    本次对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了。

    PS:和位移(translate)一样,缩放也是可以叠加的。

       canvas.scale(0.5f,0.5f);
       canvas.scale(0.5f,0.1f);

    调用两次缩放则 x轴实际缩放为0.5x0.5=0.25 y轴实际缩放为0.5x0.1=0.05

    下面我们利用这一特性制作一个有趣的图形。

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(-400,-400,400,400);   // 矩形区域
    
            for (int i=0; i<=20; i++)
            {
                canvas.scale(0.9f,0.9f);
                canvas.drawRect(rect,mPaint);
            }


    ⑶旋转(rotate)

    旋转提供了两种方法:

      public void rotate (float degrees)
      
      public final void rotate (float degrees, float px, float py)

    和缩放一样,第二种方法多出来的两个参数依旧是控制旋转中心点的。

    默认的旋转中心依旧是坐标原点:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.rotate(180);                     // 旋转180度 <-- 默认旋转中心为原点
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    改变旋转中心位置:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            RectF rect = new RectF(0,-400,400,0);   // 矩形区域
    
            mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
            canvas.drawRect(rect,mPaint);
    
            canvas.rotate(180,200,0);               // 旋转180度 <-- 旋转中心向右偏移200个单位
    
            mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
            canvas.drawRect(rect,mPaint);

    好吧,旋转也是可叠加的

         canvas.rotate(180);
         canvas.rotate(20);

    调用两次旋转,则实际的旋转角度为180+20=200度。

    为了演示这一个效果,我做了一个不明觉厉的东西:

            // 将坐标系原点移动到画布正中心
            canvas.translate(mWidth / 2, mHeight / 2);
    
            canvas.drawCircle(0,0,400,mPaint);          // 绘制两个圆形
            canvas.drawCircle(0,0,380,mPaint);
    
            for (int i=0; i<=360; i+=10){               // 绘制圆形之间的连接线
                canvas.drawLine(0,380,0,400,mPaint);
                canvas.rotate(10);
            }


    ⑷错切(skew)

    skew这里翻译为错切或者也可以叫做倾斜,错切是特殊类型的线性变换,不是很好理解,等下直接看图吧。

     /**
         * Preconcat the current matrix with the specified skew.
         *
         * @param sx The amount to skew in X
         * @param sy The amount to skew in Y
         */
        public void skew(float sx, float sy) {
            native_skew(mNativeCanvasWrapper, sx, sy);
        }
    参数含义:
    float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
    float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.

    比如我们要倾斜45°,那么tan1就是45°。

     //移动画布的原点为屏幕中心点
            canvas.translate(width/2,height/2);
            //绘画出两条坐标轴
            mPaint.setColor(Color.RED);
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawLine(-500,0,500,0,mPaint);
            canvas.drawLine(0,-800,0,800,mPaint);
            //画出矩形
            RectF rectF = new RectF(-200,-200,200,200);
            mPaint.setColor(Color.BLACK);
            mPaint.setStrokeWidth(2);
            canvas.drawRect(rectF,mPaint);
    
            //倾斜,参数为tan函数的值,比如x轴方向倾斜45°
            canvas.skew(1,0);
            mPaint.setColor(Color.BLUE);
            canvas.drawRect(rectF,mPaint);


    看到上图突然有点迷迷糊糊的样子,不明白为什么是这样拉伸和倾斜的。其实我们在倾斜的过程中,有两个参数,sx,sy对吧。这两个参数实际代表的分别是Y轴的倾斜,和X轴的倾斜,刚好和我们的坐标轴是相反的。在上图中skew(1,0)。意思是X轴倾斜Y轴倾斜45°。我们把坐标轴的Y轴方向上倾斜45°,拉伸就是现在的样子,sx正数是逆时针方向,负数,是顺时针方向。sy刚好相反。

    ⑸快照(save)和回滚(restore)

    Q: 为什存在快照与回滚
    A:画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。

    与之相关的API:

    状态栈:

    其实这个栈我也不知道叫什么名字,暂时叫做状态栈吧,它看起来像下面这样:


    什么是画布和图层?
    A:实际上我们看到的画布是由多个图层构成的,如下图(图片来自网络):

    实际上我们之前讲解的绘制操作和画布操作都是在默认图层上进行的。在通常情况下,使用默认图层就可满足需求,但是如果需要绘制比较复杂的内容,如地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)等,则分图层绘制比较好一些。你可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。

    SaveFlags

    save 有两种方法:

      // 保存全部状态
      public int save ()
      
      // 根据saveFlags参数保存一部分状态
      public int save (int saveFlags)

    可以看到第二种方法比第一种多了一个saveFlags参数,使用这个参数可以只保存一部分状态,更加灵活,这个saveFlags参数具体可参考上面表格中的内容。

    每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。

    saveLayerXxx

    saveLayerXxx有比较多的方法:

    // 无图层alpha(不透明度)通道
    public int saveLayer (RectF bounds, Paint paint)
    public int saveLayer (RectF bounds, Paint paint, int saveFlags)
    public int saveLayer (float left, float top, float right, float bottom, Paint paint)
    public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)
    
    // 有图层alpha(不透明度)通道
    public int saveLayerAlpha (RectF bounds, int alpha)
    public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
    public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
    public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)

    注意:saveLayerXxx方法会让你花费更多的时间去渲染图像(图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。

    使用saveLayerXxx方法,也会将图层状态也放入状态栈中,同样使用restore方法进行恢复。

    这个暂时不过多讲述,如果以后用到详细讲解。(因为这里面东西也有不少啊QAQ)

    restore

    状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。

    同样以上面状态栈图片为例,调用一次restore方法则将状态栈中第5次取出,根据里面保存的状态进行状态恢复。

    restoreToCount

    弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。

    以上面状态栈图片为例,如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。

    getSaveCount

    获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。

    不过请注意,该函数的最小返回值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。

    常用格式

    虽然关于状态的保存和回滚啰嗦了不少,不过大多数情况下只需要记住下面的步骤就可以了:

       save();      //保存状态
       ...          //具体操作
       restore();   //回滚到之前的状态

    这种方式也是最简单和最容易理解的使用方法。

    之前的一篇文章颜色的混合模式中使用过,当时我们保存一个新的透明图层来绘制颜色的混合效果,否则因为系统自带的属性颜色问题,会影响我们的混合效果,看一下之前的一个事例:

     //背景色设为白色,方便比较效果
            canvas.drawColor(Color.WHITE);
            //将绘制操作保存到新的图层,因为图像合成是很昂贵的操作,将用到硬件加速,这里将图像合成的处理放到离屏缓存中进行
            RectF rectF = new RectF(0,0,1000,1000);
            int saveCount = canvas.saveLayer(rectF, mPaint, Canvas.ALL_SAVE_FLAG);
            //绘制目标图
            RectF DErectF = new RectF(0,0,500,500);
            canvas.drawBitmap(dstBmp, null, DErectF, mPaint);
            //设置混合模式
            mPaint.setXfermode(mXfermode);
            //绘制源图
            RectF RSrectF = new RectF(250,250,800,800);
            canvas.drawBitmap(srcBmp, null, RSrectF, mPaint);
            //清除混合模式
            mPaint.setXfermode(null);
    
            //还原画布
            canvas.restoreToCount(saveCount);

    总结:更多的详细操作方法,可以详细的了解官网,我们后边还会继续讲解。如果喜欢我的文章可以扫描左边的微信公众号关注我。谢谢

    本文参考连接点击打开链接







    展开全文
  • canvas画布基本操作

    2020-06-09 17:24:31
    简单画直线和三角形(结合注释)–线条绘制 <body> <canvas id="canvas" width="1024" height="768" style="border: 1px solid #aaa; display: block; margin: 50px auto;"> 当前浏览器不支持canvas,...

    简单画直线和三角形(结合注释)–线条绘制

    <body>
        <canvas id="canvas" width="1024" height="768" style="border: 1px solid #aaa; display: block; margin: 50px auto;">
            当前浏览器不支持canvas,请更换浏览器后再试
        </canvas>
            
        <script>
            // canvas是基于状态绘制的
            window.onload = function () {
                /** @type {HTMLCanvasElement} */
                var canvas = document.getElementById('canvas');
                var context = canvas.getContext('2d'); // 得到绘图的上下文环境 使用context绘制
    
                // 绘图第一步 状态设置
                context.moveTo(100, 100);
                context.lineTo(700, 700); // 1 + 2 一条直线
                context.lineTo(100, 700);
                context.lineTo(100, 100); // 1 + 2 + 3 + 4 一个三角形
                context.lineWidth = 5; //线条宽度
                context.strokeStyle = "blue"; //线条样式 可以使用css任意方法赋值
                //绘制
                context.stroke(); // 笔划,用于绘制线条
            }
        </script>
    </body>
    

    效果
    在这里插入图片描述

    着色绘制

    <body>
        <canvas id="canvas" width="1024" height="768" style="border: 1px solid #aaa; display: block; margin: 50px auto;">
            当前浏览器不支持canvas,请更换浏览器后再试
        </canvas>
            
        <script>
            
            window.onload = function () {
                /** @type {HTMLCanvasElement} */
                var canvas = document.getElementById('canvas');
                var context = canvas.getContext('2d'); // 得到绘图的上下文环境 使用context绘制
    
                // 绘图第一步 状态设置
                context.moveTo(100, 100);
                context.lineTo(700, 700); // 1 + 2 一条直线
                context.lineTo(100, 700);
                context.lineTo(100, 100); // 1 + 2 + 3 + 4 一个三角形
    
                context.fillStyle = "skyblue"; // 填充颜色样式
                context.fill(); //着色
    
                // 着色后绘制线条
                context.lineWidth = 5; //线条宽度
                context.strokeStyle = "black"; //线条样式 可以使用css任意方法赋值
                context.stroke(); // 笔划,用于绘制线条
            }
        </script>
    </body>
    

    在这里插入图片描述

    绘制七巧板(无数个版都可以绘制)

    <body>
        <canvas id="canvas" style="border: 1px solid aaa; display: block; margin: 50px auto;">
            你的浏览器不支持canvas,请更换浏览器后再试
        </canvas>
    
        <script>
            var tangram = [
                {p:[{x:0,y:0},{x:800,y:0},{x:400,y:400}],color:'#caff67'},
                {p:[{x:0,y:0},{x:400,y:400},{x:0,y:800}],color:'#67becf'},
                {p:[{x:800,y:0},{x:800,y:400},{x:600,y:600},{x:600,y:200}],color:'#ef3d61'},
                {p:[{x:600,y:200},{x:600,y:600},{x:400,y:400}],color:'#f9f51a'},
                {p:[{x:400,y:400},{x:600,y:600},{x:400,y:800}],color:'#a594c0'},
                {p:[{x:200,y:600},{x:400,y:800},{x:0,y:800}],color:'#fa8ecc'},
                {p:[{x:800,y:400},{x:800,y:800},{x:400,y:800}],color:'#f6ca29'},
            ]
            window.onload = function () {
                /** @type {HTMLCanvasElement} */
                var canvas = document.getElementById('canvas');
    
                canvas.width = 800;
                canvas.height = 800;
    
                var context = canvas.getContext('2d');
                for(var i = 0; i < tangram.length; i ++){
                    draw(tangram[i], context)
                }
                
                function draw(piece, cxt){
                    cxt.beginPath();
                    cxt.moveTo(piece.p[0].x,piece.p[0].y);
                    for(var i = 1; i < piece.p.length; i ++){
                        cxt.lineTo(piece.p[i].x, piece.p[i].y);
                    }
                    cxt.closePath();
                    cxt.fillStyle = piece.color;
                    cxt.fill();
                }
            }
        </script>
    </body>
    

    效果
    在这里插入图片描述

    展开全文
  • canvas canvas是Android中自定义...这两个方法是最先应该了解的,save方法是将现在的画布状态保存,restore方法是将画布的状态退回到上个画布的状态,所以当我们画一些小的需要画布操作的图形的时候,先save再restore
  • Canvas之画布操作

    千次阅读 2016-06-01 22:50:40
    canvas 画布基本操作
  • Selenium之Canvas画布操作

    千次阅读 2019-08-12 18:11:12
    现在有一个场景是需要进入到 Canvas画布中 进行单击操作,现在使用过如下方法 # 进入画布 650 270 canvas = driver.find_element_by_xpath("//canvas[@id='#canvas']") actions.move_to_element(canvas).move_by_...
  • Canvas之画布操作

    2017-05-17 16:15:54
    版权声明:本人所有文章均采用 [知识共享 署名-非商业性使用-禁止演绎 4.0 国际 许可协议] ...Canvas之画布操作 作者微博: @GcsSloop 【本系列相关文章】 上一篇Canvas之绘制基本形状中我们了解了如何使用Canvas绘
  • html5画布操作的简单学习-简单时钟

    千次阅读 2014-12-13 11:55:43
    html5画布操作的简单学习-简单时针 一.什么是 Canvas? HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。 画布是一个矩形区域,您可以控制其每一像素。 canvas 拥有多种绘制路径、矩形、圆形、字符...
  • android开发 之 Canvas之画布操作

    千次阅读 2017-08-02 16:47:06
    一.Canvas的常用操作速查表 操作类型 相关API 备注 绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布 绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, ...
  • 上一篇Canvas之绘制基本图形中我们了解了...本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。 一.Canvas的常用操作速查表 操作类型 相关API ...
  • 1、为什么要有画布操作? 二、Canvas的基本操作 1、Translate(平移) 2、Scale(缩放) 3、Rotate(旋转) 4、skew(错切) 5、save(快照)和restore(回滚) 6、clipXXX(裁剪) 6.1、clipRect 6.2、clipPath 7、...
  • 本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。 一.Canvas的常用操作速查表 操作类型 相关API 备注 绘制...
  • 本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。 一.Canvas的常用操作速查表 操作类型 相关API 备注 绘制颜色 ...
  • 上一篇Canvas之绘制基本图形中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。 本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法...
  • 一.Canvas的常用操作速查表 操作类型 相关API 备注 绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布 绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, ...
  • canvs画布可以直接获取到图像数据,从而进行图像操作,原理很简单,但是实现起来还是会有很多坑。 1.本地图片的协议为file://且域名为空,因此在canvs中获取getImageData时会出现跨域问题 解决方法:讲图片和网页...
  • 上一篇Canvas之绘制基本形状中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。一...
  • 1.画布操作 为什么要有画布操作画布操作可以帮助我们用更加容易理解的方式制作图形。 例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做? 按照我们通常的想法(被常年训练出来的...
  • 画布操作为什么要有画布操作画布操作可以帮助我们用更加容易理解的方式制作图形。例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?按照我们通常的想法(被常年训练出来的数学思维...

空空如也

1 2 3 4 5 ... 20
收藏数 44,096
精华内容 17,638
关键字:

画布操作