美文网首页半栈工程师Android开发Android开发经验谈
Canvas中的绘图师讲解与实战——Android高级UI

Canvas中的绘图师讲解与实战——Android高级UI

作者: 9dfaf364d57f | 来源:发表于2019-05-14 13:09 被阅读12次

    目录

    一、前言

    二、如何画好一幅图

    三、Canvas的图形API

    四、画布保存状态API

    五、实战——时钟与指针

    六、写在最后

    一、前言

    在上一篇文章中,我们只是分享了裁剪类型的API,今天接着分享绘图部分API。话不多说,老规矩,先上实战图。

    时钟与指针

    image

    二、如何画好一幅图

    我们在上一篇文章中讲到了,绘制一幅图的工具和坐标系。我们继续思考,在现实中使用一张纸绘制时,我们会对这张纸进行旋转一定角度来方便自己绘制,有时为了绘制一些细节,会进行放大,有时也会进行移动这张纸。而这些操作,在canvas中也有各自对应的操作。

    1、rotate 旋转

    (1)第一个rotate函数

    public void rotate(float degrees)
    

    描述: 以原点为旋转中心,旋转画布 degrees 角度。正数为顺时针旋转,负数为逆时针旋转。

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    canvas.rotate(30);
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为旋转后绘制的图。


    image

    (2)第二个rotate函数

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

    描述: 以 (px, py) 为旋转中心,将画布旋转 degrees 角度。正数为顺时针旋转,负数为逆时针旋转。

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    canvas.rotate(30, 200, 300);
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为旋转后绘制的图。


    image

    2、scale 缩放

    (1)第一个scale函数

    public void scale(float sx, float sy)
    

    描述 : 以原点进行缩放画布,x轴缩放 sx 倍,y轴缩放 sy 倍。

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    canvas.scale(0.5f,0.33f);
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为缩放后绘制的图。

    image
    (2)第二个scale函数
    public final void scale(float sx, float sy, float px, float py)
    

    描述: 以 (px, py) 进行缩放画布,x轴缩放 sx 倍,y轴缩放 sy 倍。

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    canvas.scale(0.5f, 0.33f, 200, 300);
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为缩放后绘制的图。


    image

    3、skew 斜切

    public void skew(float sx, float sy)
    

    描述: 进行 x 轴和 y轴 的拉伸。

    拉伸规则 当一个点为(x, y)时,进行斜切变换(sx, sy),得到的结果 (rx, ry)

    1. rx = x + sx * y;
    2. ry = y + sy * x;

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    canvas.skew(1, 0.5f);
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为斜切后绘制的图。

    可以使用上面的 “拉伸规则” ,将红色框的点带入便可得到蓝色框对应的点。

    image

    4、translate 偏移

    public void translate(float dx, float dy)
    

    描述: 将画布水平移动 dx 个像素, 垂直移动 dy 个像素。

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    canvas.translate(100, 200);
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为移动后绘制的图。


    image

    5、setMatrix 矩阵

    public void setMatrix(@Nullable Matrix matrix)
    

    描述: 将矩阵作用于画布。

    举个例子

    mPaint.setColor(Color.RED);
    canvas.drawRect(mRectF, mPaint);
    
    mMatrix.preTranslate(getWidth() / 2, getHeight() / 2);
    mMatrix.preScale(2, 1);
    
    canvas.setMatrix(mMatrix);
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    红色为原图,蓝色为使用矩阵后绘制的图。

    image
    值得一提

    矩阵的内容比较多,这里只是略带一提,如果想见识见识他的真正威力,可以看看在小盆友另一篇博文放荡不羁SVG讲解与实战实战中的使用,具体代码请进传送门

    三、Canvas的图形API

    1、drawCircle 画圆

    public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
    

    描述: 在坐标为 (cx,cy) 的地方绘制半径为 radius 的圆。

    举个例子

    // 在 原点处 画半径为100的圆
    canvas.drawCircle(0, 0, 100, mPaint);
    

    效果图

    image

    2、drawOval 画椭圆

    (1)第一个drawOval函数

    public void drawOval(@NonNull RectF oval, @NonNull Paint paint)
    

    描述:oval 的矩形范围内,绘制椭圆。

    举个例子

    RectF mRectF = new RectF();
    mRectF.left = -150;
    mRectF.top = -150;
    mRectF.right = 400;
    mRectF.bottom = 150;
    
    canvas.drawOval(mRectF, mPaint);
    

    效果图

    橘色部分则为我们绘制的椭圆,而紫色框(为了方便观看而绘制出来)则是我们的 oval 的范围。

    image
    (2)第二个drawOval函数
    public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)
    

    描述:左上(left,top) 和 右下(right,bottom) 形成的矩形范围内,绘制椭圆。

    值得注意的是,这个方法只能在 API21 以上的版本 才能使用,所以建议使用第一个函数。

    举个例子

    canvas.drawOval(-150, -150, 400, 150, mPaint);
    

    效果图

    橘色部分则为我们绘制的椭圆,而紫色框(为了方便观看而绘制出来)则是我们的 oval 的范围。

    两个函数效果完全一样,只是前一个函数将两个坐标点封装在 Rect 中,而后一函数展示在函数参数中。

    image

    3、drawLine 画线

    (1)drawLine函数

    public void drawLine(float startX, float startY, float stopX, float stopY,
                @NonNull Paint paint)         
    

    描述: 在坐标 (startX, startY) 和 (stopX, stopY) 中绘制一条直线。

    举个例子

    canvas.drawLine(-200, -200,0, 0, mPaint);
    

    效果图

    image

    (2)第一个drawLines函数

    public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)
    

    描述: pts数组中每四个数构成一条直线,每四个数中前两个为起始坐标,后两个为终止坐标。如果不够四个数,则这一组不进行绘制。

    举个例子

    private float[] pts = new float[]{
                0, -400, 200, -400, // 构成上面的线
                -300, 0, -300, 300, // 构成左边的线
                0, 400, 300, 400    // 构成右边的线
        };
    
    canvas.drawLines(pts, mPaint);
    

    效果图

    image

    (3)第二个drawLines函数(带偏移)

    public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
                @NonNull Paint paint) 
    

    描述: 该方法比上一个方法多加两个参数,即偏移量和数量。偏移量offset为一时,则从pts的下标为1的地方开始进行读数,count则决定了多少个数。

    举个例子

    private float[] pts = new float[]{
                0, -400, 200, -400, 
                -300, 0, -300, 300, 
                0, 400, 300, 400    
        };
    
    canvas.drawLines(pts, 2, 8, mPaint);
    

    效果图

    pts数组中,从下标为2的数字开始,每四个数构成一条线,直到下标为 10 (由8+2得来) 的数为止。第一条线为上面的线,第二条线为下面的线。

    image

    4、drawArc 画弧

    (1)第一个drawArc函数

    public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
                @NonNull Paint paint)
    

    描述: 在 oval矩形范围内,绘制 从startAngle角度 到 sweepAngle角度的圆弧。

    参数说明:
    1)oval:圆弧所绘的矩形范围区域。
    2)startAngle:起始角度。0度时,指向为坐标系中的x轴正半轴。
    3)sweepAngle:基于 startAngle 角度,扫过的角度范围,正数则按顺时针方向,负数则按逆时针方向。
    4)useCenter:弧的两端是否要连接中心点。true连接中心点,false不连接中心点。
    5)paint:画笔。

    举个例子

    RectF mRectF = new RectF();
    mRectF.left = -150;
    mRectF.top = -150;
    mRectF.right = 400;
    mRectF.bottom = 150;
    
    canvas.drawArc(mRectF, 0, 120, true, mPaint);
    

    效果图

    橘色部分则为弧线部分,紫色则为矩形范围(为了方便查看才绘出)。

    image
    (2)第二个drawArc函数
    public void drawArc(float left, float top, float right, float bottom, float startAngle,
                float sweepAngle, boolean useCenter, @NonNull Paint paint)
    

    描述: 该方法和上一方法功能完全一样,只是用四个 float 表示 矩形的端点。

    举个例子

    canvas.drawArc(-150, -150, 400, 150, 0, 120, false, mPaint);
    

    效果图

    橘色为圆弧,紫色为矩形范围

    image

    5、drawPoint 画点

    (1)drawPoint函数

    public void drawPoint(float x, float y, @NonNull Paint paint)
    

    描述: 在坐标为 (x,y) 处绘制点

    举个例子

    mPaint.setColor(mColor1);
    mPaint.setStrokeWidth(dpToPx(5));
    canvas.drawPoint(100, 100, mPaint);
    

    效果图

    image
    (2)第一个drawPoints函数
    public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)
    

    描述: pts数组中每两个数构成一个坐标(前者为x,后者为y),并在该坐标处点。

    举个例子

    private float[] pts = new float[]{
            0, -400,
            200, -400,
            -300, 0
    };
    
    mPaint.setColor(mColor2);
    mPaint.setStrokeWidth(dpToPx(5));
    canvas.drawPoints(pts, mPaint);
    

    效果图

    image
    (3)第二个drawPoints函数(带偏移)
    public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
                @NonNull Paint paint)
    

    描述: 这个方法和上面的方法大致相同,唯一区别在于从下标为offset开始读取坐标,读取长度个数为count。

    举个例子

    private float[] pts = new float[]{
            0, -400,
            200, -400,
            -300, 0
    };
    
    mPaint.setColor(mColor2);
    mPaint.setStrokeWidth(dpToPx(5));
    canvas.drawPoints(pts, 1, pts.length - 1, mPaint);
    

    效果图

    image

    6、drawRect 画矩形

    (1)drawRect函数

    public void drawRect(@NonNull RectF rect, @NonNull Paint paint)
    public void drawRect(@NonNull Rect r, @NonNull Paint paint)
    

    描述: 在 rect 的范围内绘制矩形,两个方法的唯一区别在于第一个参数类型分别为 RectF 和 Rect。

    RectF 和 Rect 的区别:

    1. 精度不同:RectF 四个点为浮点数,Rect 四个点为整型
    2. 所包含的方法不完全相同。

    举个例子

    RectF mRectF = new RectF();
    mRectF.left = -150;
    mRectF.top = -150;
    mRectF.right = 400;
    mRectF.bottom = 150;
    
    canvas.drawRect(mRectF, mPaint);
    

    效果图

    image

    (2)drawRect函数

    public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) 
    

    描述: 在 (left,top) 和 (right,bottom) 形成的矩形范围内绘制矩形。

    举个例子

    canvas.drawRect(-150, -150, 400, 150, mPaint);
    

    效果图

    image

    7、drawRoundRect 画圆角矩形

    (1)第一个drawRoundRect函数

    public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)
    

    描述: 在 rect 范围内,绘制圆角矩形。

    参数说明:
    1)rx:水平方向的半径,下图中的橘色部分
    2)ry:竖直方向的半径,下图中的红色部分

    image
    举个例子
    canvas.drawRoundRect(mRectF, 80, 100, mPaint);
    

    效果图

    image

    (2)第二个drawRoundRect函数

    public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
                @NonNull Paint paint)
    

    描述: 与上述的方法功能完全相同,只是绘制范围由四个浮点数进行确定。

    举个例子

    canvas.drawRoundRect(-150, -150, 400, 150, 100, 50, mPaint);
    

    效果图

    image

    8、drawColor 给画布点颜色

    (1)第一个drawColor函数

    public void drawColor(@ColorInt int color)
    

    描述: 给画布绘制color颜色值。

    举个例子

    canvas.drawColor(Color.parseColor("#ffffff"));
    

    比较简单就不上效果图了。

    (2)第二个drawColor函数

    public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode) 
    

    描述: 给画布绘制颜色,会与之前的图形有 mode 的作用。

    举个例子

    Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
    
    Matrix mMatrix = new Matrix();
    mMatrix.setScale(0.25f, 0.25f);
    
    canvas.drawBitmap(mBitmap, mMatrix, mPaint);
    canvas.drawColor(Color.parseColor("#88880000"),
                    PorterDuff.Mode.DST_OVER);
    

    效果图

    image
    值得一提

    我们所介绍的第一个 drawColor(@ColorInt int color) 函数,其实最后使用了 PorterDuff.Mode.SRC_OVER 模式。

    至于 PorterDuff.Mode 的具体使用,请看小盆友的另一篇博文:图像操纵大师Xfermode讲解与实战

    9、drawRGB 给画布点颜色

    (1)drawRGB函数

    public void drawRGB(int r, int g, int b)
    

    描述: 给画布绘制颜色,按照 红(r),绿(g),蓝(b) 三原色进行组合

    举个例子

    canvas.drawARGB(255, 217, 142);
    

    (2)drawARGB函数

    public void drawARGB(int a, int r, int g, int b)
    

    描述: 给画布绘制颜色,按照 透明度(a),红(r),绿(g),蓝(b) 三原色进行组合

    举个例子

    canvas.drawARGB(200, 255, 217, 142);
    

    10、drawPath 绘制路径

    public void drawPath(@NonNull Path path, @NonNull Paint paint)
    

    描述: 将 路径path 绘制在画布上。

    举个例子
    这个方法使用的地方非常之多,例如我们绘制一个 “心” 形

    mPaint.setColor(mColor1);
    mPaint.setStyle(Paint.Style.FILL);
    // 路径的构建,移步github
    canvas.drawPath(mPath, mPaint);
    

    效果图

    image
    值得一提

    心形路径的构建使用了 贝塞尔曲线,对 贝塞尔曲线 有兴趣的童鞋,可以移步小盆友的另一篇博文:自带美感的贝塞尔曲线原理与实战

    四、画布保存状态API

    1、状态值

    在进行 API 讲解前,我们需要先说明状态值,他控制着我们要保存什么信息。

    1. MATRIX_SAVE_FLAG:保存图层的 Matrix矩阵信息
    2. CLIP_SAVE_FLAG:保存裁剪信息
    3. HAS_ALPHA_LAYER_SAVE_FLAG:保存该图层的透明度
    4. FULL_COLOR_LAYER_SAVE_FLAG:完全保留该图层颜色
    5. CLIP_TO_LAYER_SAVE_FLAG:创建图层时,会把canvas(所有图层)裁剪到参数指定的范围,如果省略这个flag将导致图层开销巨大,性能不好。
    6. ALL_SAVE_FLAG:保存所有信息

    敲黑板了!!! 虽然罗列了这么多,但1-5的FLAG已经全部被遗弃,只剩 ALL_SAVE_FLAG 这个FLAG

    2、save

    public int save()
    

    描述: 这个函数用于保存图层状态,保存此刻的 canvas 画布的所有状态(例如:原点位置,旋转角度,一切我们对canvas的操作都被保存)。

    3、saveLayer

    // saveFlags 只能是 Canvas.ALL_SAVE_FLAG
    public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
                @Saveflags int saveFlags)
          
    // saveFlags 只能是 Canvas.ALL_SAVE_FLAG      
    public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags)
    
    // API21及以上才可使用
    public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint)
    // API21及以上才可使用
    public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) 
    

    描述: 该方法与 save 一样会保存进状态栈,然后通过 restorerestoreToCount 进行恢复。不同的是该方法会创建一个新的图层

    这里创建的图层,我们可以类比为PS中的图层概念,存在意义是不会影响到其他图层的数据。例如我们在XFermode的博文中的刮刮卡的实战中,就有用到这一概念,否则我们需要看到的图片也会被一同清除。

    4、saveLayerAlpha

    // saveFlags 只能是 Canvas.ALL_SAVE_FLAG
    public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags)
          
    // saveFlags 只能是 Canvas.ALL_SAVE_FLAG  
    public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
                @Saveflags int saveFlags)
    
    // API21及以上才可使用
    public int saveLayerAlpha(@Nullable RectF bounds, int alpha) 
    // API21及以上才可使用
    public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)
    

    描述:saveLayer 相同的是会存进状态栈和创建一个图层,然后通过 restorerestoreToCount 进行恢复。不同的是创建的图层是具有透明度的,而透明度由 alpha 决定,范围为 0-255。

    5、恢复

    // 恢复
    public void restore()
    
    // 恢复至指定的 状态栈层数
    public void restoreToCount(int saveCount)
    

    描述: 这两个方法,是将上面三种方法保存的函数进行恢复。而区别在于 restore 每次从状态栈中恢复拿出一个状态恢复,而 restoreToCount恢复到指定的状态栈层数(该层也会被出栈),这个 saveCount 参数在上面三种类型的方法调用后都会进行返回各自对应的层数。

    6、小结

    先举个例子汇总一下这几个方法:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        log(canvas);
    
        int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(),
                mPaint, Canvas.ALL_SAVE_FLAG);
        log(canvas);
    
        canvas.save();
        log(canvas);
    
        canvas.saveLayer(0, 0, getWidth(), getHeight(),
                mPaint, Canvas.ALL_SAVE_FLAG);
        log(canvas);
    
        canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(),
                50, Canvas.ALL_SAVE_FLAG);
        log(canvas);
    
        canvas.translate(getWidth() / 2, getHeight() / 2);
        canvas.drawRect(mRect, mPaint);
    
        canvas.restore();
        log(canvas);
    
        canvas.restoreToCount(layer);
        log(canvas);
    
    }
    
    private void log(Canvas canvas) {
        Log.i("canvas", "canvas count:" + canvas.getSaveCount());
    }
    

    输出结果

    image
    我们从代码和输出结果可以得出以下几个结论:
    1. 初始状态下,状态栈中便有一个默认的状态;
    2. 在不创建图层的情况下,所有操作都是作用于默认图层;
    3. 使用 restoreToCount(x) 进行恢复,会连同x层出栈;

    一图胜千言:

    将上面的代码转换成图,就如下效果


    image

    五、实战——时钟与指针

    1、效果图

    image
    github地址:传送门

    2、编程思路

    我们先拆解下这幅图,其实构成的为三部分:

    1. 一个圆圈
    2. 刻度
    3. 指针

    我们逐一解决:

    (1)一个圆圈

    这个我们信手拈来,canvas就有绘制圆的 API,我们在第三节的一小点就讲到了

    canvas.drawCircle(0, 0, width / 2, mPaint);
    

    (2)刻度

    对于刻度,其实有两种画法:

    • 第一种:是听起来比较 “高大上” ,使用三角函数算出每个刻度的起始坐标和终止坐标,然后进行绘制。
    • 第二种:较为机灵,使用我们在 第二小节的第一点 介绍的 rotate 进行一点点的旋转画布,然后绘制线。

    (3)指针

    我们需要先构建下图中蓝色的路径作为指针,由一段圆弧和两条线构成。

    image
    构建思路:

    第一步:在红色的矩形内,绘制圆弧(使用了第三小节第四点)
    第二步:从圆弧的左点绘制线到图中红色顶点
    第三部:从红色顶点绘制线到圆弧右点,最后关闭路径path

    具体代码如下:

    mPointerPath.moveTo(mPointerRadius, 0);
    // 第一步
    mPointerPath.addArc(mPointerRectF, 0, 180);
    // 第二步
    mPointerPath.lineTo(0, -width / 4);
    // 第三步
    mPointerPath.lineTo(mPointerRadius, 0);
    mPointerPath.close();
    

    (4)开启旋转

    我们只需要通过属性动画,让指针动起来即可。而指针的旋转只需要通过让画布旋转即可,也就是用到第二小节第一点的rotate

    canvas.save();
    canvas.rotate(mCurAngle);
    
    ... 省略创建指针
    
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPointerColor);
    canvas.drawPath(mPointerPath, mPaint);
    canvas.restore();
    

    时钟与指针 完整代码:传送门

    六、写在最后

    这次介绍的是canvas最为基础的API操作,但其实越为基础的东西,越容易被忽略也越是进阶中最需要的部分。这次写的时间耗时较久,主要是API较多,写demo和截图比较频繁。

    如果你觉得文章对你有所帮助,请给我一个赞并关注我吧。如果发现有那些欠妥的地方,请留言区与我讨论,我们共同进步。

    高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄

    欢迎加我微信,我们可以进行更多更有趣的交流


    image

    相关文章

      网友评论

        本文标题:Canvas中的绘图师讲解与实战——Android高级UI

        本文链接:https://www.haomeiwen.com/subject/zjlsaqtx.html