AndroidUi之Canvas,Path

        画布,在我们android的View或者自定义View中占有举足轻重的地位,在android中它的意义也是字面的意思,我们可以通过画笔绘制几何图形,文本,路径或者位图。我们可以将Canvas的API主要分为三类,一类是绘制相关的,例如drawText,drawLine...,一类是变换相关的,canvas.translate、canvas.rotate....,还有一类是状态保存和恢复相关的如canvas.save、canvas.restore....下面开始分别讲解。

绘制相关

    和画布绘制相关的也就是我们在Canvas类中定义的类似drawXXXX以draw开头的方法:

AndroidUi之Canvas,Path_第1张图片

上图截取了一部分Canvas中的drawXXX的方法。

绘制文本drawText

     我们平时使用TextView的时候都是调用setText方法将我们需要显示的文本内容显示在TextView上面,其实最终也是调用Canvas的drawText方法。先看下面的方法,特地将text的初始坐标放在(0,15)这个位置上,看一下效果:

private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setTextSize(30);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.RED);
        canvas.drawText("测试DrawText的功能的Demo",0,15,mPaint);
    }

AndroidUi之Canvas,Path_第2张图片

效果就是绿色的文本只显示了一部分,这是为什么呢。其实在android中当我们使用canvas.drawText的时候,设置的坐标的位置其实是设置的文字的左下角那边的位置。为什么这么说呢?大家小学的时候,都用过那种作业本,汉语拼音,四线三格,我们都是以第三条线位标准写拼音的,Android的标准也是差不多的。看下图:

AndroidUi之Canvas,Path_第3张图片

与字体相关的在上图标注的几个基准线:

baseline:字符基准线(其实就是我们上面drawText标注的(0,15)这个15的值)

ascent:字符最高点到baseLine的推荐距离。

top:字符最高点到baseline之间的距离。

descent:字符最低点到baseline之间的推荐距离

bottom:字符最低点到baseline之间的最大距离

leading:行间距,即前一行的descent到当前行的ascent之间的距离。

与绘制文本相关的还有FontMetrics,可以获取上面我们标记解释的几个值:

  public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

我们可以通过 下面方法来获得,并读取上面的属性:

Paint.FontMetrics metrics = mPaint.getFontMetrics();

文字的测量

paint中提供给了测量两种测量文字的方法,但是有一些细小的差别:分别位measureText()和getTextBounds()方法,他们都属于Paint的方法,两个函数的区别在于:

        如果你用代码分别使用getTextBounds() 和 measureText() 来测量文字的宽度,你会发现 measureText() 测出来的宽度总是要比getTextBounds()d的 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。

  • getTextBounds(): 它测量的是文字的显示范围(关键词:显示)。就是能够包裹住这段文字的嘴角的矩形,就是这段文字的 bounds。

  • measureText() : 它测量的是文字绘制时所占用的宽度。绘制文字的时候,往往需要占用比他的实际显示宽度更多一点的宽度,以此来让文字和文字之间保留一些间距,不会显得过于拥挤,也就是设置我们不同行文字之间的间距,导致了 measureText() 比getTextBounds()测量出的宽度要大一些

drawRect,drawCicle,drawBitmap,drawArc...等等都是比较简单的,直接使用API就ok了。

Path

    path即路径,可以用来绘制直线,曲线,以及这些直线或曲线构成的集合图形,还可用于根据路径绘制文字。常用的API有移动,连线,闭合,添加图形等....

 private void init() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(6);
        mPaint.setColor(Color.RED);
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.moveTo(100,70);    //路径起始点移动到(100,70)的位置
        mPath.lineTo(140,800);   //从(100,70)向(140,800)画一条线
        mPath.lineTo(250,600);   //从(140,800)向(250,600)画一条线
        mPath.close();   //将线路闭合
        canvas.drawPath(mPath,mPaint);
    }

AndroidUi之Canvas,Path_第4张图片

上图的效果是这样的,其实我们可以将mPath.lineTo(140,800)修改为mPath.rLineTo(40,730),rLineTo的意思其中r就是“相对位置”的意思,就是相对于path的上一个所画路径的结束点的相对位置也即是140-100 = 40,800-70=730;上图我们看到红色的图像是闭合的,有人可以以为使是调用mPath.close的结果,其实上例中如果价格mPath.close去掉,也是这样的结果,主要问题出在Paint的style上面,上例中将style设置为FILL,如果修改为STROKE的话

AndroidUi之Canvas,Path_第5张图片        AndroidUi之Canvas,Path_第6张图片

上面2图体现的结果不一样,图1未封闭,图2成为一个闭合的图像:

 private void init() {
        mPaint = new Paint();
        //修改画笔的样式 为Style
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(6);
        mPaint.setColor(Color.RED);
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.moveTo(100,70);
        mPath.lineTo(140,800);
        mPath.lineTo(250,600);
        //图一需要去掉下面一句代码
         mPath.close();
        canvas.drawPath(mPath,mPaint);
    }

上图中设置画笔都为STROKE,左图1注释掉mPath.close(), 右图2加上mPath.close()方法,结果就不一样了。path.close() 方法的意思就是,将路径闭合,如果路径本来就是闭合的就不用管,如果不是闭合的就将首尾两点连线闭合。

Path.addArc

  添加一个弧形。最终调用的都是含有6个参数的addArc方法,left,top表示的是弧形所在矩形的左上方的位置坐标,right,bottom表示弧形所在矩形的右下方的位置坐标,startAngle就是弧形开始的位置,sweepAngel表示弧形扫过的角度:

 public void addArc(RectF oval, float startAngle, float sweepAngle) {
        addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
    }

public void addArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle) {
     
    }
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addArc(200,200,400,400,-225,225);
        mPath.close();
        canvas.drawPath(mPath,mPaint);
    }

AndroidUi之Canvas,Path_第7张图片

Path.addRect

      addRect添加一个矩形,addRect方法中的参数除了矩形的left,top,right,bottom的坐标外,还有一个参数表示绘制的顺序:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addRect(500,500,900,900,Path.Direction.CW);
        canvas.drawPath(mPath,mPaint);
    }

Path.Direction.CW   顺时针方向绘制

Path.direction.CCW  逆时针方向绘制

其他的mPath.addCircle,mPath.addOval 都需要这个参数

Path追加图形

例如下面一段代码:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addArc(200,200,400,400,-225,225);
        mPath.arcTo(400,200,600,400,-180,225,false);
        canvas.drawPath(mPath,mPaint);
    }

AndroidUi之Canvas,Path_第8张图片

 

上例中首先调mPath.addArc画一个弧形,然后调用mPath.arcTo在原来的图像上面追加一个弧形,类似前面的mPath.lineTo方法,在arcTo方法中最后一个参数为false,

boolean forceMoveTo

也就是forceMoveTo = false; 这个参数的意思是绘制的时候,是否移动起点的位置。当forceMoveTo = true,表示绘制的时候将起点移动到所要绘制的图像的起点位置,当forceMoveTo = false的时候,意思就是不移动起点,但是必须绘制一条从前一个图像的终点 指向 即将绘制的图像的起点位置的直线:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.moveTo(0,0);
        mPath.lineTo(100,100);
        mPath.arcTo(400,200,600,400,0,270,true);
        canvas.drawPath(mPath,mPaint);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.lineTo(100,100);
        mPath.arcTo(400,200,600,400,0,270,false);
        canvas.drawPath(mPath,mPaint);
    }

AndroidUi之Canvas,Path_第9张图片      AndroidUi之Canvas,Path_第10张图片

左图为 forceMoveTo = true 的效果,右图为 forceMove = false的效果。

Path.addPath

 还可以在一条路径上面,通过addPath将另外一条路径添加到当前的Path中

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //添加一个路径
        mPath.moveTo(100, 70);
        mPath.lineTo(140, 180);
        mPath.lineTo(250, 330);
        mPath.lineTo(400, 630);
        mPath.lineTo(100, 830);

        Path newPath = new Path();
        newPath.moveTo(100, 1000);
        newPath.lineTo(600, 1300);
        newPath.lineTo(400, 1700);
        mPath.addPath(newPath);
        canvas.drawPath(mPath,mPaint);
    }

Path添加多阶贝塞尔曲线

      贝塞尔曲线是用一系列点来控制曲线状态的,将这些点分为两类,一类是数据点,一类是控制点。

       在很多绘图或者动画效果中需要用到贝塞尔曲线,在API中为我们提供了一阶,二阶,三阶贝塞尔曲线的API。其实一节贝塞尔曲线就是一条直线用lineTo就可以绘制,二阶贝塞尔曲线用quadTo或者rQuadTo(相对位置)方法绘制,三阶贝塞尔曲线由cubicTo 或者rCubicTo方法进行绘制,例如下面是绘制二阶贝塞尔曲线的一般使用方法

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画二阶贝塞尔曲线
        mPath.moveTo(300, 500);
        mPath.quadTo(500, 100, 800, 500);
        //参数表示相对位置,等同于上面一行代码
      //  mPath.rQuadTo(200, -400, 500, 0);
        canvas.drawPath(mPath,mPaint);
    }

AndroidUi之Canvas,Path_第11张图片

  • 一阶曲线原理

一阶曲线是没有控制点的,仅有两个数据点(A 和 B),最终效果一个线段。

一阶公式如下:

  

  • 二阶曲线原理

  二阶曲线由两个数据点(A 和 C),一个控制点(B)来描述曲线状态,大致如下:

          

连接DE,取点F,使得: ,这样获取到的点F就是贝塞尔曲线上的一个点,动态图如下:

二阶公式如下:

二阶贝塞尔曲线公式推倒方法如下:

AndroidUi之Canvas,Path_第12张图片

三阶贝塞尔曲线去下:

公式如下:

高阶贝塞尔曲线的通用公式如下,可以认为是一个递归的操作过程

变换相关

Canvas中有许多与变换相关的API,可以帮助我们事先许多意想不到的效果。

canvas.translate

canvas.translate使画布平移,先看下面的代码,画两个矩形,一个红色矩形坐标为(50,50),(400,400) 改变画笔颜色之后,画绿色矩形,坐标还为(50,50),(400,400),看效果:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(50,50,400,400,mPaint);
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(50,50,400,400,mPaint);
    }

AndroidUi之Canvas,Path_第13张图片

看上面的图像,绿色矩形在红色矩形的下方,再看下方代码和效果基本上可以看出平移的一些套路了

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(50,50,400,400,mPaint);
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(50,50,400,400,mPaint);
        canvas.translate(-50,-50);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(50,50,400,400,mPaint);
    }

AndroidUi之Canvas,Path_第14张图片

画红色矩形的坐标为(50,50),(400,400),但是canvas.translate(100,100)之后画绿色矩形,我们看出同样是(50,50),(400,400)的坐标,但是绿色矩形和红色矩形没有重合,归结原因是画布平移之后,Canvas坐标原点平移到(100,100),然后再画绿色的矩形,实际上画的绿色的矩形"相对于"原来红色矩形的坐标系应该为(100+50,100+50),(100+400,100+400)。蓝色矩形同理。可以得出一个结论,画笔平移方法translate(x,y)方法传递x,y的值为 正数 画布坐标原点向下平移,传递的值为负数,画布坐标原点想上平移

canvas.scale

canvas.scale可以对画布进行缩放,有2个重载的方法:

    public void scale(float sx, float sy) {
        if (sx == 1.0f && sy == 1.0f) return;
        nScale(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) {
        if (sx == 1.0f && sy == 1.0f) return;
        translate(px, py);
        scale(sx, sy);
        translate(-px, -py);
    }

先看第一个函数,传入float sx,float sy是缩放比率:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.scale(0.5f,0.5f);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

 

AndroidUi之Canvas,Path_第15张图片

我们看到调用scale(0.5f,0.5f)之后,同样坐标的红色矩形和绿色矩形,为啥绿色矩形会出现那样的位置呢。我个人对这个的理解是:其实我们画同样大小的红色和绿色大小的矩形,但是显示出的大小结果不一样了,就像我们查看百度地图或者高德地图一样,当我们缩小地图的时候,我们看到的范围其实更大了,其实是我们显示地图的比率变了而已。类比上面的效果,当我们画(200,200),(700,700)的红色矩形,我们的坐标系,可以理解为一个像素点为1cm,那么红色矩形就是宽高为500cm的矩形,但是当我们将画布缩小之后,我们一个单位像素点就变成了1/0.5 = 2cm,所以我们的绿色图形的宽高就为原来的一半,起始坐标相对于原来的画布变为(100,100)。所以按照这个理解,我们可以推测出canvas.scale(2,2)的效果为绿的图形放大了。起始坐标为(400,400).

AndroidUi之Canvas,Path_第16张图片

第二个方法   public final void scale(float sx, float sy, float px, float py) 传递的是四个参数:

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);       
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.scale(0.5f,0.5f,200,200);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

AndroidUi之Canvas,Path_第17张图片

看到最后的效果是这样的。起始上面的代码的四个参数的scale方法等同于下面的代码

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.translate(200,200);
        canvas.scale(0.5f,0.5f);
        canvas.translate(200,200);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

那么 public final void scale(float sx, float sy, float px, float py) 的意思就是画布先平移translate(px,py)然后,scale(0.5f,0.5f),最后  translate(-px,-py).

canvas.rotate

canvas.rotate(float degress),传递的参数为度数:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //旋转操作
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.rotate(45);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
    }

AndroidUi之Canvas,Path_第18张图片

看到上面的绿色矩形旋转了45°,有一部分旋转到屏幕之外了。。当canvas.rotate(degree)传入的 度数为正数的时候是绕顺时针旋转,当传入的度数为负数的时候,绕逆时针旋转,默认是绕画布的坐标原点旋转,上例就是绕(0,0)顺时针旋转45°。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //旋转操作
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.rotate(-45,450,450);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

 

AndroidUi之Canvas,Path_第19张图片

上例中canvas.rotate(-45,450,450) 后面两个参数是旋转的中心450,450,画布绕(450,450)逆时针旋转45度。

canvas.skew(float sx,float sy)

画布的错切(倾斜),sx,sy值,分别表示将画布在x方向和y方向上倾斜角度的tan 值。

当sx=1时,即将画布在x方向上旋转45度,其实就是x轴保持方向不变,y轴逆时针旋转45度。

当sy=1时,即将画布在y方向上旋转45度,其实就是y轴保持方向不变,x轴顺时针旋转45度。

当sx、sy都改变时,两者都进行相应的移动。

例如下面的代码:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       //倾斜 错切操作
        canvas.drawRect(0,0,200,200,mPaint);
        canvas.skew(1,0);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,200,200,mPaint);
    }

AndroidUi之Canvas,Path_第20张图片

红色矩形为错切之前的矩形,将画布向x方向倾斜45°之后,然后再绘制绿色矩形,可能不是很好理解,看下面一幅图:

AndroidUi之Canvas,Path_第21张图片

红色线为原先的x轴和y轴,当调用canvas.skew(1,0)之后,向x方向错切45°,其实就是将y轴以x为轴线旋转45°,然后再画矩形.如果上面图中不画红色矩形直接错切之后的效果:一切只是视角不一样而已

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       //倾斜 错切操作
        canvas.skew(1,0);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,200,200,mPaint);
    }

AndroidUi之Canvas,Path_第22张图片

canvas.clipXXX

    canvas.clipXXX系列方法可以对画布进行裁剪,使裁剪之后额图形只能在裁剪的区域内绘制,否则 “无效”。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.GRAY);    //绘制灰色背景
        canvas.drawRect(0,0,500,500,mPaint);  //画一个矩形区域为(0,0) (500,500)
        canvas.clipRect(new Rect(0,0,200,200));   //裁剪(0,0) (200,200)的区域
        mPaint.setColor(Color.GREEN);  //画笔设置为绿色
        canvas.drawRect(0,0,200,200,mPaint);  //绘制(0,0) (200,200)的绿色矩形
        canvas.drawRect(250,250,400,400,mPaint);  //绘制(250,250) (400,400)的绿色矩形
    }

效果图为:

AndroidUi之Canvas,Path_第23张图片

看到(250,250),(400,400)的绿色矩形并没有显示出来,是因为我们裁剪的大小区域为(0,0),(200,200) 而我们的(250,250),(400,400)的矩形区域不在上面的矩形区域中,所以并没有绘制出来。

canvas还有其他的裁剪方法,例如canvas.clipPath,和clipRect类似,只不过clipPath能够裁剪的图形多一些。clipOutRect反向裁剪。也就是说裁剪之后绘制的图像,只能显示在裁剪区域之外,在裁剪区域之内的不能显示出来。

canvas.setMatrix

setMatrix其实可以做上面的平移 旋转,缩放 等等操作。使用Matrix进行变换的一般操作步骤为:

  1. 创建 Matrix 对象;
  2. 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //matrix变换
        canvas.drawRect(0,0,400,400,mPaint);
        Matrix matrix = new Matrix();
        matrix.setTranslate(200,200);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,400,400,mPaint);
    }
  1. Canvas.setMatrix(matrix):用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换。
  2. Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。
  3. 使用matrix时候 有的时候需要调用matrix.reset()清除之前的变换。

状态保存和恢复

   Canvas调用translate,scale,rotate,skew,matrix等的变换之后,后续的操作都是基于变换之后的Canvas的,都会受到影响,对后续的操作很不方便,Canvas提供了save,saveLayer,saveLayerAlpha,restore,restorToCount等方法来保存和恢复状态。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.drawRect(200,200,500,500,mPaint);
    }

AndroidUi之Canvas,Path_第24张图片

两个矩形的大小虽然一致,但是明显画布平移之后,坐标零点变换位置了,变为原来画布的(100,100)位置,如果我们想回到原来的坐标零点,需要在调用translate(-100,-100)才能将绿色矩形调整至原来的坐标零点。并且后面的(200,200),(500,500)的绿色矩形也是在画布平移之后的画布的基础上绘制的。Android中Canvas提供一些保存状态的API,让后续的绘制可以不受这些变换的影响。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        canvas.save();
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        mPaint.setColor(Color.BLUE);
        canvas.restore();
        canvas.drawRect(0,0,300,300,mPaint);
    }

AndroidUi之Canvas,Path_第25张图片

上述代码中调用了canvas.store()  和 canvas.restore()方法,store的意思就是保存当前的画布状态,restore就是恢复之前的画布状态,这里也就是恢复到tanslate(100,100)之前的画布状态,然后再绘制蓝色的矩形,就是在坐标零点了。

Android中一般情况下store和restore是成对出现的,意思为保存当前状态,退回到上一步的画布状态。例如下面多次成对出现这个两个方法(restore方法总是退回到最近的store方法调用之前的画布的状态)。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        canvas.save();
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.save();
        canvas.translate(100,100);
        canvas.drawLine(0,0,500,500,mPaint);

        canvas.restore();
        canvas.restore();
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(0,0,300,300,mPaint);
    }

AndroidUi之Canvas,Path_第26张图片

先画红色矩形,调用store,然后平移画布绘制绿色矩形,然后再调用store然后再平移画布,绘制绿色直线,最后两次调用restore,回到画布最开始的状态,在坐标零点绘制蓝色矩形。有点类似一个 “入栈出栈” 的过程 。还有一种restoreToCount的方法可以一步到位。

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        int saveState = canvas.save();
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.translate(100,100);
        canvas.drawLine(0,0,500,500,mPaint);
        canvas.restoreToCount(saveState);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(0,0,300,300,mPaint);
    }

调用 int saveState = canvas.save() ; 保存当前的状态,然后调用canvas.restoreToCount(saveState) 直接回退到那个状态。出栈操作就是直接回退到指定的栈的那一层。

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //创建离屏绘制图层
        int layerId = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);

        //  在图层之间进行绘制      

        //最后将创建的图层绘制到canvas中
        canvas.restoreToCount(layerId);
    }

下面看一个引导页的效果实现:

AndroidUi之Canvas,Path_第27张图片

 

多看几遍,我们可以将效果图分解为几个阶段:

1.6个小球绕中心 旋转的阶段

2.6个小球 以球所在的中心 先扩大 后收缩的阶段。

3.类似水波纹扩散的阶段。

自定义View的成员变量

    private Paint mPaint;  //画小圆的画笔
    private Paint mHolePaint;   //最后白色的波浪的画笔
    private int colors [] = null;
    private int mCenterX,mCenterY;
    private float mCircleRadius = 18;  //旋转的小球的半径
    private float mCurrentAngle = 0;  //小球旋转的时候的初始角度,做动画使用
    private float mDiatance;


    private SplashState mState;  //当前的状态
    private ValueAnimator mValueAnimator;

    private float mRotateRadius = 100;  //旋转大圆的半径
    //当前大圆的半径
    private float mCurrentRotateRadius = mRotateRadius;
    //扩散圆的半径
    private float mCurrentHoleRadius = 0F;

定义上面分析的集中状态:

   //抽象内部类  描述上面3种状态
   private abstract class SplashState{
        abstract void drawState(Canvas canvas);
    }

    //小球的旋转状态
    private class RotateState extends  SplashState{

        .....
    }

   //小球的扩散聚合状态
    private class MerginState extends  SplashState{

        .....
    }

    //扩散 水波纹状态
    private class SpreadState extends  SplashState{

        .....
    }

自定义View中最重要的onDraw方法,但是看起来却那么简单:第一个状态也就是旋转状态

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mState == null){
            mState = new RotateState();
        }
        mState.drawState(canvas);
    }

看旋转状态的RotateState的实现

private class RotateState extends  SplashState{
        public RotateState(){

            mValueAnimator = ValueAnimator.ofFloat(0f, (float) (Math.PI*2));
        //    mValueAnimator.setRepeatCount(1);
            mValueAnimator.setInterpolator(new LinearInterpolator());
            mValueAnimator.setDuration(1500);
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mCurrentAngle = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new MerginState();
                }
            });
            mValueAnimator.start();
        }
        @Override
        void drawState(Canvas canvas) {
            drawBackground(canvas);
            drawCicle(canvas);
        }
    }

定义属性动画,使小球旋转起来 看drawCicle方法:

 private void drawCicle(Canvas canvas){
        int length = colors.length;
        float angle = (float)((2*Math.PI)/length);
        for(int i=0;i

drawBackground是在3个状态中都要绘制的:

 private void drawBackground(Canvas canvas){
        if(mCurrentHoleRadius>0){  //扩散半径>0的时候 就是最好一个状态
            float strokeWidth = mDiatance - mCurrentHoleRadius;  //线宽在减小
            float radius = strokeWidth / 2 + mCurrentHoleRadius; //半径一直在增大
            mHolePaint.setStrokeWidth(strokeWidth);
            canvas.drawCircle(mCenterX,mCenterY, radius, mHolePaint);
        }else{
            canvas.drawColor(Color.WHITE);  //否则 只绘制白色的背景
        } 
    }

上面的float radius  =  strokeWidth / 2 + mCurrentHoleRadius; 需要加上线宽的一半 ,否则白色圆扩散的半径就接不上了,这里为啥,可以看之前的AndroidUi之Paint 在最开始讲解了线宽 对绘制圆的影响。

注意:在MerginState的时候,执行属性动画时候,调用了

//反转执行动画...
mValueAnimator.reverse();
     public MerginState(){
            mValueAnimator = ValueAnimator.ofFloat(0,mRotateRadius);
            mValueAnimator.setDuration(1000);
            mValueAnimator.setInterpolator(new OvershootInterpolator(5));
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mCurrentRotateRadius = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new SpreadState();
                }
            });
            //反转执行动画...
            mValueAnimator.reverse();
        }

本来原来的属性动画,是旋转半径从0-mRotateRadius  并且拦截器为 OvershootInterpolator。现在调用reverse效果之后,从最大半径mRotateRadius的位置到0。

Demo中还有2个自定义View的效果,一个是爆炸效果,一个是类似QQ消息气泡效果,可以自行查看。

AndroidUi之Canvas,Path_第28张图片      AndroidUi之Canvas,Path_第29张图片

 

Demo下载地址

 

 

 

 

你可能感兴趣的:(AndroidUi之Canvas,Path)