目录

坐标系以及view的位置信息

API简介

Canvas基本操作

Canvas变化

save和restore

分层的概念layer

总结


坐标系以及view的位置信息

         自定义view在平时有很多应用,我们知道自定义view,其实就是通过Canvas进行绘制,但是在绘制之前,一些基本的知识要明确,安卓的坐标系和我们实际在数学中用的坐标系还有一些区别, 在安卓中初始化以屏幕的左上角为原点,

android 控件坐标工具 android canvas坐标_缩放

           这是关于坐标的解释,还有我们再绘画view的时候。经常用到view的长宽,left,top,right,bottom的坐标。view获取左,上,右下这些坐标都是基于parent的基础,是指相对parent的距离。不是相对于整个屏幕的。

android 控件坐标工具 android canvas坐标_canvas旋转缩放_02

          通过procession画的,还处于摸索阶段,如果哪位同学知道画数学函数之类好用的软件可以推荐一下。

API简介

        作用

                                                    Api

                               说明

绘制颜色

drawColor, drawRGB, drawARGB

通过ARGB设置画布颜色

绘制基本形状

drawPoints, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc,drawPath

绘制点,线,长方形,圆形,椭圆,弧形,以及路径

绘制图片

drawBitmap, drawPicture

绘制位图

绘制文本

drawText, drawPosText, drawTextOnPath

绘制各式文本,其中根据路径绘制比较常用

画布裁剪

clipPath, clipRect

可以设置画布的展示区域

画布状态

save, restore, saveLayerXxx, restoreToCount, getSaveCount

此部分api就是保存图层状态、 回滚到指定状态、 获取保存次数,可以看成类似git保存版本状态

画布变换

translate, scale, rotate, skew

位移、缩放、 旋转、错切

Matrix

getMatrix, setMatrix, concat

实际上画布的位移,缩放等操作的都是图像矩阵Matrix,通过C++源码可以看出。在这里google已经把matrix封装好,但是如果需要特殊效果,需要自己使用matrix操作,比如窗口抖动等效果没有封装好的api可用

Canvas基本操作

         要画东西,你需要4个基本组件:一个用来容纳像素的位图,一个用来承载画布的画布绘制调用(写入位图),一个绘制素材(例如Rect,

路径,文本,位图),和画笔(描述颜色和样式画)。其中关于绘制基本形状没有什么可说的。其中绘制弧度:

android 控件坐标工具 android canvas坐标_canvas旋转缩放_03

        这里是画了一个圆弧,其实画圆弧就是截取一个长方形内切椭圆的一段弧度,代码如下:

@Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawArc(canvas);
    }

    private void drawArc(Canvas canvas){

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        mPaint.setAntiAlias(true);

        //表示空心,这样画出的是线,否则就是实心的图形
        mPaint.setStyle(Paint.Style.STROKE);

        canvas.translate(500,500);
        RectF rf = new RectF(0,0,300,300);

        canvas.drawArc(rf,0,-80,false,mPaint);
        canvas.drawRect(rf,mPaint);
    }

       画出这个长方形是为了更好的理解弧度的由来。这里需要注意的有几点,1 Rect和RectF都是表示长方形,只是RectF参数是float,Rect参数是int。 2 画长方形他的四个参数分别是,左,上,右,下,要注意底部的坐标必须大于顶部,右部必须大于左部。 3 在ondraw千万不允许用new函数,因为ondraw会频繁调用,如果使用new,分配大量内存,会造成内存抖动,这里只是为了演示代码,所以使用了new。

       对于path其实可以看出一连串的点连接而成。 其中还可以包括贝塞尔曲线之类的。我们可以根据曲线来画文字,比如

android 控件坐标工具 android canvas坐标_canvas旋转缩放_04

       代码如下:

private void drawTexts(Canvas canvas){
        Path paths  = new Path();
        canvas.translate(500,500);
        RectF rf = new RectF(0,-400,400,0);
        paths.addArc(rf, 60, 180);
        canvas.drawPath(paths,mPaint);
        mPaint.setTextSize(50);
        canvas.drawTextOnPath("中国人民万岁", paths, 0, -20, mPaint);
    }

  drawTextOnPath中的3,4个参数是指文字相对path水平和竖直方向的位移。

Canvas变化

      我们可以大致把canvas的变化分为四类,位移(translate),旋转(rotate),缩放(scale),倾斜(skew).

 1  translate 位移比较简单。 就是将当前的原点一定到指定的x,y的位置。

比如:

 

android 控件坐标工具 android canvas坐标_canvas详解_05

private void drawTranslate(Canvas canvas){
        //初始的时候原点为(0,0),画一个圆心为(200,200)半径为100的圆
        mPaint.setColor(Color.GREEN);
        canvas.drawCircle(200,200,100,mPaint);

        //将原点移动到200,200
        canvas.translate(200,200);

        //移动之后(200,0)就相当于移动前,(400,200.)
        mPaint.setColor(Color.parseColor("#ff00ff"));
        canvas.drawCircle(200,0,100,mPaint);
    }

 移动是可以叠加的。 第二次移动是以第一次移动后的原点位置为标准。其他以此类推。

2 scale,缩放。 api提供了2中缩放的方法:

public void scale(float sx, float sy) {
        if (sx == 1.0f && sy == 1.0f) return;
        nScale(mNativeCanvasWrapper, sx, sy);
  }

   
  public final void scale(float sx, float sy, float px, float py) {
        if (sx == 1.0f && sy == 1.0f) return;
        translate(px, py);
        scale(sx, sy);
        translate(-px, -py);
  }

 可以看出两种缩放的方式,第一个就直接进行缩放,第二种是以px,py为原点进行缩放。区别可看如下代码:

  我们做如下效果的缩放:

android 控件坐标工具 android canvas坐标_缩放_06

private void drawScale(Canvas canvas){

        canvas.translate(400,800);
        mPaint.setColor(Color.RED);

        RectF rt = new RectF(0,-300,400,0);
       //简单的 画2条线当做X,Y轴
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);

        //首先画出黑色的基础矩形
        mPaint.setColor(Color.BLACK);
        canvas.drawRect(rt,mPaint);

        //保存当前设置
        canvas.save();
        //将坐标轴按x轴,y轴分别扩大1.3倍,然后再用蓝色画这个矩形,
        //会发现蓝色矩形长宽都是原来基础矩形的1.3被,当这里设置(0,1)时缩小,(1,+∞)时扩大
        canvas.scale(1.3f,1.3f);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(rt,mPaint);
        //恢复保存以前的状态。就是canvas没有放大前的状态,和save成对出现
        canvas.restore();

        canvas.save();
        //以200,0为原点进行缩放,将x,y缩小为原来的0.5倍,然后再画。
        //其实他是分为三步,第一个进行唯一,translate(200,0),然后缩放scal(0.5,0.5)
        //然后在进行位移(-200,0);但是第二次位移在缩放的基础上了。所以再位移-200,并没有回到原来的缩放前的原点。
        //因为是在(200,0)的基础上唯一,x轴缩小0.5,位移-200,变为-100.所以实际上如果没有缩放,他是在(200,0)
        //的基础上进行为(-100,0)的位移。所以虽然看着(200,0)和(-200,0)正好互补,但是因为缩放的存在不能回到原来了。
        //所以红色的矩形如图所示的位置
        canvas.scale(0.5f,0.5f,200,0);
        mPaint.setColor(Color.RED);
        canvas.drawRect(rt,mPaint);
        //再次回到save之前的状态,即canvas没有做任何变化的状态
        canvas.restore();


        canvas.save();
        //(2,-0.5)x轴变为原来的2倍,y轴首先变为原来的0.5被,然后需要y轴的反转。
        canvas.scale(2,-0.5f);
        mPaint.setColor(Color.parseColor("#ff00ff"));
        canvas.drawRect(rt,mPaint);
        //画一个紫色的变化后的矩形。如图所示
        canvas.restore();

    }

           我们看最初展示的android源码可以看出。scale(float sx, float sy, float px, float py)是先进行位移再旋转,然后再次位移。translate(px, py)移动的物理距离分别是px和py,经过scale(sx, sy)缩放后再通过translate(-px, -py)位移,移动的物理距离就是-px*sx和-py*sy。

 所以我们可以看出缩放情况如下:

倍数(n)

说明

(-∞, 0)

首先对x,y轴进行n倍伸缩,然后对应的坐标轴

(0,+∞)

直接将对应的x,y轴进行n倍伸缩

n==1

根据代码可以看出不做任何变化

3 rotate(旋转),旋转同样提供了2中方法

public void rotate(float degrees) {
        if (degrees == 0.0f) return;
        nRotate(mNativeCanvasWrapper, degrees);
    }

   
 public final void rotate(float degrees, float px, float py) {
        if (degrees == 0.0f) return;
        translate(px, py);
        rotate(degrees);
        translate(-px, -py);
    }

         第一种是直接以原点为中心进行旋转degrees度, 第二种是以px,py为原点进行旋转。它也是分为3步,1是进行位移(px,py),然后旋转,之后再进行位移(-px,-py),同理,旋转之后,表面看两次位移正好互补,但是不能回到原来的点了。首先来看以原点为标准旋转

android 控件坐标工具 android canvas坐标_自定义view_07

坐标轴变了,所有的坐标点也就变了,计算的时候。 你可以仍然以水平为X轴思考,无论是画线,还是图形。在原来X轴上的操作,旋转之后会和X轴一样旋转。比如下图:

android 控件坐标工具 android canvas坐标_缩放_08

private void drawRotate(Canvas canvas){

        canvas.translate(400,800);
        mPaint.setColor(Color.RED);
        RectF rt = new RectF(0,-300,400,0);
        //简单的 画2条线当做X,Y轴
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
        canvas.drawCircle(500,-100,100,mPaint);


        mPaint.setStrokeWidth(30);
        mPaint.setColor(Color.GREEN);
        canvas.drawPoint(0,0,mPaint);

        mPaint.setStrokeWidth(5);
        canvas.save();
        //旋转60度
        canvas.rotate(60);
        mPaint.setColor(Color.RED);
        //画x,y轴还有圆形的代码和旋转一直一模一样
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
        canvas.drawCircle(500,-100,100,mPaint);
        canvas.restore();
    }

    通过看代码我们会发现。 绘画黑色的坐标轴与圆形, 和旋转之后画红色的坐标与圆形的代码一模一样。 所以无论怎样旋转,任何图形与其坐标轴的相对位置不会发生改变。这就是网上很多圆形进度条的原理。如下效果:

android 控件坐标工具 android canvas坐标_canvas详解_09

这种气势就是画一条水平的线,然后不停的旋转。

4 倾斜skew

public void skew(float sx, float sy) {
        if (sx == 0.0f && sy == 0.0f) return;
        nSkew(mNativeCanvasWrapper, sx, sy);
    }

参数sx,sy分别是x,y轴上倾斜角度的tan值。比如skew(1,1)则分别是切斜45度

android 控件坐标工具 android canvas坐标_canvas旋转缩放_10

这是x轴切斜45度的效果,代码如下:

private void drawSkew(Canvas canvas){
        canvas.translate(400,800);
        RectF rt = new RectF(0,-300,400,0);
        //简单的 画2条线当做X,Y轴
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);

        canvas.drawRect(0,0,200,200,mPaint);

        canvas.skew(1,0);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,200,200,mPaint);
    }

         其实可以把canvas的操作看成是画很多层,移动之后就属于在另一层重新画,无论怎样移动,都可以想象成再水平面操作。因为所有的绘画元素与原点的相对位置不变。

        需要注意一点,绘画的移动,缩放,旋转,倾斜,都是可以连续多次进行的。多次操作效果叠加。

save和restore

        实际开发中绘画的时候经常会有移动,缩放等操作。但是变换完成之后,还想恢复到原来的状态。再进行绘制。如果没有save,和restore,比如我们translate(100,20),操作完成之后,还要进行translate(-100,-20)进行复原,这还是一次操作。如果多次操作就会很麻烦。canvas提供save和restore解决了这个问题。save就相当于git中保存状态,restore就相当于回滚操作。回到save的状态。

android 控件坐标工具 android canvas坐标_缩放_11

private void drawSaveRestore(Canvas canvas){

        //画一个以300,300位圆心,100位半径的圆,这个时候坐标原点为左上角(0,0)
        mPaint.setColor(Color.RED);
        canvas.drawCircle(300,300,100,mPaint);

        mPaint.setStrokeWidth(15);
        canvas.drawLine(0,0,getRight()-100,0,mPaint);
        canvas.drawLine(0,0,0,getBottom()-100,mPaint);

        mPaint.setStrokeWidth(8);
        //保存这个时候的状态。即原点为(0,0)的状态
        canvas.save();
        //进行移动,将原点移动到(500,500)的位置
        canvas.translate(500,500);
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(0,0,500,0,mPaint);
        canvas.drawLine(0,0,0,500,mPaint);

        //画一个以(500,500)为原点长宽都是200的正方形
        canvas.drawRect(0,0,200,200,mPaint);

        //恢复到save的状态。这个时候,坐标原点又是(0,0)了,
        canvas.restore();
        mPaint.setColor(Color.parseColor("#ff00ff"));
        canvas.drawRect(0,0,200,200,mPaint);
    }

          save和restore是成对出现的。是一对一的。canvas还提供了restoreToCount(int saveCount)方法,去恢复到指定的保存状态。我们可以查看源码。一切说的很清楚。

/**
     * Efficient way to pop any calls to save() that happened after the save
     * count reached saveCount. It is an error for saveCount to be less than 1.
     *
     * Example:
     *    int count = canvas.save();
     *    ... // more calls potentially to save()
     *    canvas.restoreToCount(count);
     *    // now the canvas is back in the same state it was before the initial
     *    // call to save().
     *
     * @param saveCount The save level to restore to.
     */
    public void restoreToCount(int saveCount) {
        if (saveCount < 1) {
            if (!sCompatibilityRestore || !isHardwareAccelerated()) {
                // do nothing and throw without restoring
                throw new IllegalArgumentException(
                        "Underflow in restoreToCount - more restores than saves");
            }
            // compat behavior - restore as far as possible
            saveCount = 1;
        }
        nRestoreToCount(mNativeCanvasWrapper, saveCount);
    }

其实我们可以把save操作当做入栈操作,然后restore可以简单看做是出栈操作。但是save都有编号,也可以指定编号进行恢复。

android 控件坐标工具 android canvas坐标_缩放_12

分层的概念layer

     如果做过地图的项目大家都清楚。在标志某个地点或者其他类似效果。会通过层的概念在指定地点添加覆盖物。canvas也有分层的概念,通过savelayer的方式保存, 

android 控件坐标工具 android canvas坐标_自定义view_13

代码如下:

private void drawLayer(Canvas canvas) {
        canvas.translate(100, 100);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(75, 75, 75, mPaint);
        canvas.saveLayerAlpha(0, 0, 200, 200, 0x88);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(125, 125, 75, mPaint);
        canvas.restore();
    }

在savexxx的函数中,存在flag的参数,在api28中已经全部改为:ALL_SAVE_FLAGS,其他已经无效:

/**
     * Restore everything when restore() is called (standard save flags).
     * <p class="note"><strong>Note:</strong> for performance reasons, it is
     * strongly recommended to pass this - the complete set of flags - to any
     * call to <code>saveLayer()</code> and <code>saveLayerAlpha()</code>
     * variants.
     *
     * <p class="note"><strong>Note:</strong> all methods that accept this flag
     * have flagless versions that are equivalent to passing this flag.
     */
    public static final int ALL_SAVE_FLAG = 0x1F;

    private static void checkValidSaveFlags(int saveFlags) {
        if (sCompatiblityVersion >= Build.VERSION_CODES.P
                && saveFlags != ALL_SAVE_FLAG) {
            throw new IllegalArgumentException(
                    "Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed");
        }
    }

通过这里的源码可以看出。

总结

        其实view的绘制原理是一个很复杂的过程。相信的大家都可以大致说出,measure,layout,draw这三个流程,此篇只是简单介绍canvas的应用,甚至不用去管他的流程,先明白怎样画的即可。所有复杂的view绘制,其实都可以拆分成简单的操作,绘制可以从简单做起。