自定义view (一) Android Canvas详解

目录

座标系以及view的位置信息

API简介

Canvas基本操作

Canvas变化

save和restore

分层的概念layer

总结


  • 座标系以及view的位置信息

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

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

          通过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,

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

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

   @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其实可以看出一连串的点连接而成。 其中还可以包括贝塞尔曲线之类的。我们可以根据曲线来画文字,比如

       代码如下:

 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的位置。

比如:

 

 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为原点进行缩放。区别可看如下代码:

  我们做如下效果的缩放:

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),同理,旋转之后,表面看两次位移正好互补,但是不能回到原来的点了。首先来看以原点为标准旋转

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

  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();
    }

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

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

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度

这是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的状态。

 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都有编号,也可以指定编号进行恢复。

  • 分层的概念layer

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

代码如下:

 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绘制,其实都可以拆分成简单的操作,绘制可以从简单做起。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章