自定义View第三章:Canvas

本文是参考 https://www.gcssloop.com/ 仅仅用于个人学习及帮助记忆。

  • 一:Canvas之绘制图形

Canvas我们可以称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,非常强大。

Canvas的常用操作速查表
详解
  • 绘制颜色:
    绘制颜色是填充整个画布,常用于绘制底色。
canvas.drawColor(Color.BLUE); //绘制蓝色
  • 创建画笔:
    要想绘制内容,首先需要先创建一个画笔,如下:
// 1.创建一个画笔
private Paint mPaint = new Paint();
// 2.初始化画笔
private void initPaint() {
    mPaint.setColor(Color.BLACK);       //设置画笔颜色
    mPaint.setStyle(Paint.Style.FILL);  //设置画笔模式为填充
    mPaint.setStrokeWidth(10f);         //设置画笔宽度为10px
    mPaint.setAntiAlias(true);            // 抗锯齿
}
// 3.在构造函数中初始化
public SloopView(Context context, AttributeSet attrs) {
   super(context, attrs);
   initPaint();
}
  • 绘制点
    可以绘制一个点,也可以绘制一组点
canvas.drawPoint(200, 200, mPaint);     //在坐标(200,200)位置绘制一个点
canvas.drawPoints(new float[]{          //绘制一组点,坐标位置由float数组指定
      500,500,
      500,600,
      500,700
},mPaint);
  • 绘制直线
    绘制直线需要两个点,初始点和结束点,同样绘制直线也可以绘制一条或者绘制一组
canvas.drawLine(300,300,500,600,mPaint);    // 在坐标(300,300)(500,600)之间绘制一条直线
canvas.drawLines(new float[]{               // 绘制一组线 每四数字(两个点的坐标)确定一条线
    100,200,200,200,
    100,300,200,300
},mPaint);
  • 绘制矩形
    我们都知道,确定一个矩形最少需要四个数据,就是对角线的两个点的坐标值,这里一般采用左上角和右下角的两个点的坐标。
    关于绘制矩形,Canvas提供了三种重载方法,第一种就是提供四个数值(矩形左上角和右下角两个点的坐标)来确定一个矩形进行绘制。 其余两种是先将矩形封装为Rect或RectF(实际上仍然是用两个坐标点来确定的矩形),然后传递给Canvas绘制,如下:
// 第一种
canvas.drawRect(100,100,800,400,mPaint);
// 第二种
Rect rect = new Rect(100,100,800,400);
canvas.drawRect(rect,mPaint);
// 第三种
RectF rectF = new RectF(100,100,800,400);
canvas.drawRect(rectF,mPaint);

看到这里,相信很多观众会产生一个疑问,为什么会有Rect和RectF两种?两者有什么区别吗?
答案当然是存在区别的,两者最大的区别就是精度不同,Rect是int(整形)的,而RectF是float(单精度浮点型)的。除了精度不同,两种提供的方法也稍微存在差别,在这里我们暂时无需关注,想了解更多参见官方文档 Rect 和 RectF

  • 绘制圆角矩形
    绘制圆角矩形也提供了两种重载方式
// 第一种
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);
// 第二种
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);

下面简单解析一下圆角矩形的几个必要的参数的意思。
很明显可以看出,第二种方法前四个参数和第一种方法的RectF作用是一样的,都是为了确定一个矩形,最后一个参数Paint是画笔,无需多说,与矩形相比,圆角矩形多出来了两个参数rx 和 ry,这两个参数是干什么的呢?
稍微分析一下,既然是圆角矩形,他的角肯定是圆弧(圆形的一部分),我们一般用什么确定一个圆形呢?
答案是圆心 和 半径,其中圆心用于确定位置,而半径用于确定大小。
由于矩形位置已经确定,所以其边角位置也是确定的,那么确定位置的参数就可以省略,只需要用半径就能描述一个圆弧了。
但是,半径只需要一个参数,但这里怎么会有两个呢?
好吧,让你发现了,这里圆角矩形的角实际上不是一个正圆的圆弧,而是椭圆的圆弧,这里的两个参数实际上是椭圆的两个半径,他们看起来个如下图:
红线标注的 rx 与 ry 就是两个半径,也就是相比绘制矩形多出来的那两个参数。

通过计算可知我们上次绘制的矩形宽度为700,高度为300,当你让 rx大于350(宽度的一半), ry大于150(高度的一半) 时奇迹就出现了, 你会发现圆角矩形变成了一个椭圆, 他们画出来是这样的(见上图二)

// 矩形
RectF rectF = new RectF(100,100,800,400);  
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);
// 绘制圆角矩形
mPaint.setColor(Color.BLUE);
canvas.drawRoundRect(rectF,700,400,mPaint);

其中灰色部分是我们所选定的矩形,而里面的圆角矩形则变成了一个椭圆,实际上在rx为宽度的一半,ry为高度的一半时,刚好是一个椭圆,通过上面我们分析的原理推算一下就能得到,而当rx大于宽度的一半,ry大于高度的一半时,实际上是无法计算出圆弧的,所以drawRoundRect对大于该数值的参数进行了限制(修正),凡是大于一半的参数均按照一半来处理。

  • 绘制椭圆
    相对于绘制圆角矩形,绘制椭圆就简单的多了,因为他只需要一个矩形作为参数:
    绘制椭圆实际上就是绘制一个矩形的内切图形
    如果你传递进来的是一个长宽相等的矩形(即正方形),那么绘制出来的实际上就是一个圆。
// 第一种
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
// 第二种
canvas.drawOval(100,100,800,400,mPaint);
  • 绘制圆
    绘制圆形有四个参数,前两个是圆心坐标,第三个是半径,最后一个是画笔。
canvas.drawCircle(500,500,400,mPaint);  // 绘制一个圆心坐标在(500,500),半径为400 的圆。
  • 绘制圆弧
// 第一种
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}
// 第二种
public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {}

从上面可以看出,相比于绘制椭圆,绘制圆弧还多了三个参数:
startAngle  // 开始角度
sweepAngle  // 扫过角度
useCenter   // 是否使用中心
//-------------------------------------------------------
绘制椭圆
RectF rectF = new RectF(100,100,800,400);
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);
// 绘制圆弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF,0,90,false,mPaint);

RectF rectF2 = new RectF(100,600,800,900);
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF2,mPaint);
// 绘制圆弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF2,0,90,true,mPaint);
//-------------------------------------------------------
绘制正圆
RectF rectF = new RectF(100,100,600,600);
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);
// 绘制圆弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF,0,90,false,mPaint);

RectF rectF2 = new RectF(100,700,600,1200);
// 绘制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF2,mPaint);
// 绘制圆弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF2,0,90,true,mPaint);

上述代码实际上是绘制了一个起始角度为0度,扫过90度的圆弧,两者的区别就是是否使用了中心点,
可以发现使用了中心点之后绘制出来类似于一个扇形,而不使用中心点则是圆弧起始点和结束点之间的连线加上圆弧围成的图形。
结果如下:

  • 二:Canvas之画布操作

为什么要有画布操作?
画布操作可以帮助我们用更加容易理解的方式制作图形。
例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?
按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。
然而这是否是被固有思维禁锢了?
假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?
合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。
PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。

  • 位移(translate)
    translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。
    请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(getResources().getColor(mColor[0]));
        canvas.translate(100, 100);
        canvas.drawCircle(0, 0, 50, mPaint);

        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.translate(100, 100);
        canvas.drawCircle(0, 0, 50, mPaint);

        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.translate(100, 100);
        canvas.drawCircle(0, 0, 50, mPaint);

ps 案例中的虚线是Guideline参考线
  • 缩放(scale)
    缩放提供了两个方法,这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。
public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)

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



缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴

        mPaint.setAntiAlias(true);                            //抗锯齿
        mPaint.setStyle(Paint.Style.FILL);                    //设置画笔样式-填充
        mPaint.setColor(getResources().getColor(mColor[0]));  //设置画笔颜色
        canvas.translate(mWidth / 2, mHeight / 2);            // 将坐标系原点移动到画布正中心

        RectF rectF = new RectF(0, -300, 300, 0);             // 矩形区域
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.scale(0.5f, 0.5f);
        canvas.drawRect(rectF, mPaint);(图一)
        /**====================================================**/
        RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.scale(-0.5f, -0.5f);
        canvas.drawRect(rectF, mPaint);(图二)
        /**====================================================**/
        RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[3]));
        canvas.scale(-1.5f, -1.5f);
        canvas.drawRect(rectF, mPaint);(图三)
        /**====================================================**/
        RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[4]));
        canvas.scale(-0.5f, -0.5f, 100, 0);
        canvas.drawRect(rectF, mPaint);(图四)
        /**====================================================**/
        RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.scale(-0.5f, -0.5f);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.scale(-0.5f, -0.5f);
        canvas.drawRect(rectF, mPaint);(图五)
        /**====================================================**/
         RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.scale(0.5f, 0.5f);
        canvas.drawRect(rectF, mPaint);
        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.scale(-1f, -1f);
        canvas.drawRect(rectF, mPaint);(图六)
图一
图二
图三
图四
图五
图六

上面案例可以得出结果:

  • 当缩放比例为负数的时候会根据缩放中心轴进行翻转,缩放中心轴就是坐标轴
  • 对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了。(图四)
  • 和位移(translate)一样,缩放也是可以叠加的。(图五图六)
  • 缩放叠加是是X轴缩放值和Y轴缩放值相乘后的结果
    图五第一次缩放是 canvas.scale(-0.5f, -0.5f),第二次缩放是 canvas.scale(-0.5f * -0.5f, -0.5f * -0.5f);
    所以第二次实际缩放是 canvas.scale(0.25f,0.25f);
    图六同理。正负相乘得负数,第二次缩放后翻转。

利用缩放叠加特性制作一个有趣的图形

        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE); //描边
        mPaint.setStrokeWidth(5f);           //设置画笔宽度为5px
        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.translate(mWidth / 2, mHeight / 2);
        for (int i = 0; i <= 20; i++) {
            canvas.scale(0.9f, 0.9f);
            canvas.drawCircle(0,0,400, mPaint);
        }
        /**====================================================**/
         RectF rectF = new RectF(-400, -400, 400, 400);
        for (int i = 0; i <= 20; i++) {
            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)
  
//案例
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(getResources().getColor(mColor[0]));
        canvas.translate(mWidth / 2, mHeight / 2);
        RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);

        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.rotate(90);
        canvas.drawRect(rectF, mPaint);

        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.rotate(90);
        canvas.drawRect(rectF, mPaint);

        mPaint.setColor(getResources().getColor(mColor[3]));
        canvas.rotate(90);
        canvas.drawRect(rectF, mPaint);
       /**====================================================**/
       mPaint.setColor(getResources().getColor(mColor[0]));
        canvas.translate(mWidth / 2, mHeight / 2);
        RectF rectF = new RectF(0, -300, 300, 0);
        canvas.drawRect(rectF, mPaint);
        
        mPaint.setColor(getResources().getColor(mColor[4]));
        canvas.rotate(180, 150, 0);
        canvas.drawRect(rectF, mPaint);

由案例得出结果:

  • rotate旋转是顺时针旋转
  • rotate旋转可以叠加
    上面虽然三次旋转都是90度,实际第二次旋转是180度,第三次是270度
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);//描边
        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.translate(mWidth / 2, mHeight / 2);
        canvas.drawCircle(0, 0, 400, mPaint); //画三个圆
        canvas.drawCircle(0, 0, 300, mPaint);
        canvas.drawCircle(0, 0, 200, mPaint);

        for (int i = 0; i <= 360; i += 10) {               // 绘制圆形之间的连接线
            canvas.drawLine(0, 200, 0, 400, mPaint);
            canvas.rotate(10);
        }
  • 错切(skew)
    skew这里翻译为错切,错切是特殊类型的线性变换。
public void skew (float sx, float sy)

参数含义:
float sx:sx 将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,其实就是将y逆时针旋转相应的角度
float sy:sy 将画布在y方向上倾斜相应的角度,sy倾斜角度的tan值,其实就是将x顺时针旋转相应的角度

        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);//描边
        mPaint.setStrokeWidth(2);
        mPaint.setColor(getResources().getColor(mColor[1]));
        canvas.translate(mWidth / 2, mHeight / 2);

        //画x轴和y轴
        canvas.drawLine(0, 0, mWidth, 0, mPaint);
        canvas.drawLine(0, 0, 0, mHeight, mPaint);

        RectF rectF = new RectF(0, 0, 150, 150);
        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.drawRect(rectF, mPaint);
        //水平错切 图一
        canvas.skew(1, 0);
        mPaint.setColor(getResources().getColor(mColor[3]));
        canvas.drawRect(rectF, mPaint);
        /**====================================================**/
        RectF rectF = new RectF(0, 0, 150, 150);
        mPaint.setColor(getResources().getColor(mColor[2]));
        canvas.drawRect(rectF, mPaint);
        //垂直错切 图二
        canvas.skew(0, 1);
        mPaint.setColor(getResources().getColor(mColor[3]));
        canvas.drawRect(rectF, mPaint);

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

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

API



解析
A:状态栈
其实这个栈我也不知道叫什么名字,暂时叫做状态栈吧,可以存储画布状态和图层状态。
B:画布和图层
实际上我们看到的画布是由多个图层构成的。在通常情况下,使用默认图层就可满足需求,但是如果需要绘制比较复杂的内容,如地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)等,则分图层绘制比较好一些。
你可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。

C:save
可以看到第二种方法比第一种多了一个saveFlags参数,使用这个参数可以只保存一部分状态,更加灵活。
每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。

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

D:saveLayerXxx
saveLayerXxx有比较多的方法。
注意:saveLayerXxx方法会让你花费更多的时间去渲染图像(图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。
使用saveLayerXxx方法,也会将图层状态也放入状态栈中,同样使用restore方法进行恢复。

// 无图层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)

E:restore
状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。
同样以上面状态栈图片为例,调用一次restore方法则将状态栈中第5次取出,根据里面保存的状态进行状态恢复。
F:restoreToCount
弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。
以上面状态栈图片为例,如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。
G:getSaveCount
获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。
不过请注意,该函数的最小返回值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。
H:常用格式
虽然关于状态的保存和回滚啰嗦了不少,不过大多数情况下只需要记住下面的步骤就可以了:

save();      //保存状态
...          //具体操作
restore();   //回滚到之前的状态
  • 三:Canvas之图片文字

绘制图片

绘制有两种方法,drawPicture(矢量图) 和 drawBitmap(位图)

  • drawPicture ==================
    使用Picture前请关闭硬件加速,以免引起不必要的问题!
    使用Picture前请关闭硬件加速,以免引起不必要的问题!
    使用Picture前请关闭硬件加速,以免引起不必要的问题!

    在AndroidMenifest文件中application节点下添上 android:hardwareAccelerated="false"以关闭整个应用的硬件加速。更多请参考这里:Android的硬件加速及可能导致的问题
    关闭整个应用硬件加速依然不行的话参考Android中Canvas绘制图形不显示
    我的mPicture.draw(canvas);直接崩溃 报错
    Picture playback is only supported on software canvas
    其他方法也不显示图形通过 xml添加android:layerType="software"全部得以解决

既然是drawPicture就要了解一下什么是Picture。 顾名思义,Picture的意思是图片。
不过嘛,我觉得这么用图片这个名词解释Picture是不合适的,为何这么说?请看其官方文档对Picture的解释:
A Picture records drawing calls (via the canvas returned by beginRecording) and can then play them back into Canvas (via draw(Canvas) or drawPicture(Picture)).For most content (e.g. text, lines, rectangles), drawing a sequence from a picture can be faster than the equivalent API calls, since the picture performs its playback without incurring any method-call overhead.
我也看不懂,于是有道翻译了下:
图片记录绘图调用(通过画布返回的beginRecording),然后可以播放他们回到画布(通过绘制(画布)或绘制图片(图片))。对于大多数内容(例如文本、线条、矩形),从图片中绘制序列比等效的API调用要快,因为图片在回放时不会产生任何方法调用开销。
恩,大概能看懂了..
Canvas绘制点,线,矩形等诸多操作用Picture录制下来,下次需要的时候拿来就能用,使用Picture相比于再次调用绘图API,开销是比较小的,也就是说对于重复的操作可以更加省时省力。
PS:你可以把Picture看作是一个录制Canvas操作的录像机。
了解了Picture的概念之后,我们再了解一下Picture的相关方法。


getWidth和getHeight没什么好说的,最后两个已经废弃也自然就不用关注了
beginRecording 和 endRecording 是成对使用的,一个开始录制,一个是结束录制,两者之间的操作将会存储在Picture中。

  //1:创建picture和paint
  private Paint mPaint = new Paint();
  private Picture mPicture = new Picture();
  // 2.录制内容方法
  private void recording() {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setColor(getResources().getColor(mColor[2]));
        Canvas canvas = mPicture.beginRecording(200, 200);  //开始录制
        // 在Canvas中具体操作
        canvas.translate(250,250);
        canvas.drawCircle(0, 0, 200, mPaint);
        mPicture.endRecording();                           //结束录制
    }
  // 3.在使用前调用(我在构造函数中调用了)
  public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        recording();
    }
 //4.onDraw里面使用
  @Override
  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       // mPicture.draw(canvas);
       // canvas.drawPicture(mPicture, new RectF(0, 0, 100, 200));
        PictureDrawable drawable = new PictureDrawable(mPicture);
        drawable.setBounds(0,0,100,200);
        drawable.draw(canvas);
    }

Picture虽然方法就那么几个,但是具体使用起来还是分很多情况的,由于录制的内容不会直接显示,就像存储的视频不点击播放不会自动播放一样,同样,想要将Picture中的内容显示出来就需要手动调用播放(绘制),将Picture中的内容绘制出来可以有以下几种方法:

使用Picture提供的draw方法绘制。(图一)
mPicture.draw(canvas);  
这种方法在比较低版本的系统上绘制后可能会影响Canvas状态,所以这种方法一般不会使用。
/**=====================================================================**/
使用Canvas提供的drawPicture方法绘制。(图二)
public void drawPicture (Picture picture)
public void drawPicture (Picture picture, Rect dst)
public void drawPicture (Picture picture, RectF dst)
和使用Picture的draw方法不同,Canvas的drawPicture不会影响Canvas状态。
注意:绘制的内容会根据选区进行缩放。
/**=====================================================================**/
将Picture包装成为PictureDrawable,使用PictureDrawable的draw方法绘制。(图三)
// 包装成为Drawable
PictureDrawable drawable = new PictureDrawable(mPicture);
// 设置绘制区域 -- 注意此处所绘制的实际内容不会缩放
drawable.setBounds(0,0,250,mPicture.getHeight());
// 绘制
drawable.draw(canvas);
此处setBounds是设置在画布上的绘制区域,并非根据该区域进行缩放,也不是剪裁Picture,每次都从Picture的左上角开始绘制。
  • drawBitmap ================
    获取Bitmap方式:
    A:通过Bitmap创建,复制一个已有的Bitmap(新Bitmap状态和原有的一致) 或者 创建一个空白的Bitmap(内容可改变)
    B:通过BitmapDrawable获取,从资源文件 内存卡 网络等地方获取一张图片并转换为内容不可变的Bitmap
    C:通过BitmapFactory获取,从资源文件 内存卡 网络等地方获取一张图片并转换为内容不可变的Bitmap
    通常来说,我们绘制Bitmap都是读取已有的图片转换为Bitmap绘制到Canvas上。
    第1和第2种方式不推荐,第3种方法我们会比较详细的说明一下如何从各个位置获取图片。

通过BitmapFactory从不同位置获取Bitmap:
A:资源文件(drawable/mipmap/raw)
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.raw.bitmap);
B:资源文件(assets)

Bitmap bitmap=null;
try {
    InputStream is = mContext.getAssets().open("bitmap.png");
    bitmap = BitmapFactory.decodeStream(is);
    is.close();
} catch (IOException e) {
    e.printStackTrace();
}

C:内存卡文件
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");
D:网络文件

// 此处省略了获取网络输入流的代码
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();

既然已经获得到了Bitmap,那么就开始本文的重点了,将Bitmap绘制到画布上。
依照惯例先预览一下drawBitmap的常用方法:

// 第一种 (图一)
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
第一种方法中后两个参数(matrix, paint)是在绘制的时候对图片进行一些改变,如果只是需要将图片内容绘制出来只需要如下操作就可以了:
canvas.drawBitmap(bitmap,new Matrix(),new Paint());
PS:图片左上角位置默认为坐标原点。
// 第二种(图二)==============================================================
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
第二种方法就是在绘制时指定了图片左上角的坐标(距离坐标原点的距离):
注意:此处指定的是与坐标原点的距离,并非是与屏幕顶部和左侧的距离, 虽然默认状态下两者是重合的,但是也请注意分别两者的不同。
canvas.drawBitmap(bitmap,200,500,new Paint());
// 第三种(图三四五)==============================================================
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)
第三种方法比较有意思,上面多了两个矩形区域(src,dst),这两个矩形选区是干什么用的?
Rect src            指定绘制图片的区域
Rect dst/RectF dst  指定图片在屏幕上显示(绘制)的区域

// 将画布坐标系移动到画布中央
canvas.translate(mWidth/2,mHeight/2);
// 指定图片绘制区域(左上角的四分之一)
Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight()/2);
// 指定图片在屏幕上显示的区域
Rect dst = new Rect(0,0,200,400);
// 绘制图片
canvas.drawBitmap(bitmap,src,dst,null);

上面是以绘制该图为例,用src指定了图片绘制部分的区域,即图五中红色方框标注的区域。
然后用dst指定了绘制在屏幕上的绘制,即图四中蓝色方框标注的区域,图片宽高会根据指定的区域自动进行缩放。
图一
图二
图三
图四

图五

从上面可知,第三种方法可以绘制图片的一部分到画布上,这有什么用呢?
如果你看过某些游戏的资源文件,你可能会看到如下的图片(图片来自网络):

用一张图片包含了大量的素材,在绘制的时候每次只截取一部分进行绘制,这样可以大大的减少素材数量,而且素材管理起来也很方便,同时也节省了图片文件头、文件结束块以及调色板等占用的空间。很类似web前端的精灵图。
下面是利用drawBitmap第三种方法制作的一个简单示例:
资源文件如下:

最终效果如下 点击此处查看源码

绘制文字

常用方法:

// 第一类
public void drawText (String text, float x, float y, Paint paint)
public void drawText (String text, int start, int end, float x, float y, Paint paint)
public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
public void drawText (char[] text, int index, int count, float x, float y, Paint paint)

// 第二类
public void drawPosText (String text, float[] pos, Paint paint)
public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)

// 第三类
public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
public void drawTextOnPath (char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)

PS 其中的CharSequence和String的区别可以到这里看看. ->戳这里<-
绘制文字部分大致可以分为三类:
第一类只能指定文本基线位置(基线x默认在字符串左侧,基线y默认在字符串下方)。
第二类可以分别指定每个文字的位置。
第三类是指定一个路径,根据路径绘制文字。
通过上面常用方法的参数也可看出,绘制文字也是需要画笔的,而且文字的大小,颜色,字体,对齐方式都是由画笔控制的。
这里仅简单介绍几种常用方法,具体在讲解Paint时再详细讲解。

  • 第一类(drawText)
第一类可以指定文本开始的位置,可以截取文本中部分内容进行绘制。
其中x,y两个参数是指定文本绘制两个基线,示例:
mPaint.setColor(Color.BLACK);        // 设置颜色
mPaint.setStyle(Paint.Style.FILL);   // 设置样式
mPaint.setTextSize(50);              // 设置字体大小
mPaint.setAntiAlias(true);
canvas.drawLine(200, 300, mWidth, 300, mPaint);  //横线
canvas.drawLine(200, 300, 200, mHeight, mPaint); //竖线
String str = "abcdefgpq";            // 文本(要绘制的内容)
// 参数分别为 (文本 基线x 基线y 画笔)
canvas.drawText(str,200,300,textPaint);
/**===============================================================**/
除了能指定绘制文本的起始位置,还能只取出文本中的一部分内容进行绘制。
截取文本中的一部分,对于String和CharSequence来说只指定字符串下标start和end位置(注意:0<= start < end < str.length())
字符  a   b   c   d   e   f   g   p   q
下标  0   1   2   3   4   5   6   7   8
假设我我们指定star为1,end为3,那么最终截取的字符串就是"bc"
使用start和end指定的区间是前闭后开的,即包含start指定的下标,而不包含end指定的下标
String str = "abcdefgpq";          // 文本(要绘制的内容)
// 参数分别为 (字符串 开始截取位置 结束截取位置 基线x 基线y 画笔)
canvas.drawText(str,1,3,200,300,textPaint);
/**===============================================================**/
对于字符数组char[]我们截取字符串使用起始位置(index)和长度(count)来确定。
同样,我们指定index为1,count为3,那么最终截取到的字符串是"bcd".
/ 字符数组(要绘制的内容)
char[] chars = "abcdefgpq".toCharArray();
// 参数为 (字符数组 起始坐标 截取长度 基线x 基线y 画笔)
canvas.drawText(chars,1,3,200,300,textPaint);
  • 第二类(drawPosText)
    通过和第一类比较,我们可以发现,第二类中没有指定x,y坐标的参数,而是出现了这样一个参数float[] pos。这个名为pos的浮点型数组就是指定坐标的,至于为啥要用数组嘛,因为这家伙野心比较大,想给每个字符都指定一个位置。这个方法现在已经废弃了,了解下就好,不推荐使用。
       // 文本(要绘制的内容)
        String value = "233333";
        float[] pos = {100, 100, 200, 200, 300, 300, 400, 400, 500, 500, 600, 600};
        // 参数分别为 (文本 基线x 基线y 画笔)
        canvas.drawPosText(value, pos, mPaint);

public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
同理索引,长度截取字符
  • 第三类(drawTextOnPath)
    待补充

你可能感兴趣的:(自定义View第三章:Canvas)