自定义View(Canvas)

上一篇我们讲了一些基本的图形绘制
http://www.jianshu.com/p/b335377d2dd9
提到了图形绘制就像我们平时画图一样,需要两个工具,纸和笔。Paint就是相当于笔,而Canvas就是纸,这里叫画布。比如圆形,矩形,文字等相关的都是在Canvas里生成。
细心的朋友可能会发现上一期乱入了这个东西canvas.translate(0,100);这是啥呢?其实这就是对画布的操作(平移)。我们知道,android手机原点是在手机的左上角,当我们想要在手机屏幕中心绘制图形的时候。需要计算坐标比较麻烦。这个时候我们可以把整个坐标系(Canvas)平移到屏幕中心。这样比如canvas.drawPoint(0,0,mPaint);点就出现在屏幕中心了

事实上,我们昨天绘制图形也是使用Canvas的drawXxx()方法绘制出来的。
Canvas所提供的各种方法根据功能来看大致可以分为几类:

第一是以drawXXX为主的绘制方法;
第二是以clipXXX为主的裁剪方法;
第三是以scale、skew、translate和rotate组成的Canvas变换方法;
第四类则是以saveXXX和restoreXXX构成的画布锁定和还原;
还有一些其他的就不归类了。
如果用到的不能硬件加速的方法,请务必关闭硬件加速,否则可能会产生不正确的效果。(待日后弄清楚再回来补充)

这里要说明的是,除了做了第四类操作,否则画布的操作是不可逆的。而且也不能对已经画上去的图形造成影响

平移(translate)

public void translate(float dx, float dy)

参数:
float dx:X轴方向上的移动距离
float dx:Y轴方向上的移动距离

注意:位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动
在牢记这一点以后,我们看一段简单的代码:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);//将画布移动到屏幕中心

        //将要绘制的矩形
        Rect rect = new Rect(0,0,200,100);
        //坐标系
        float [] pts = new float[]{0,0,mWidth/2,0,0,0,0,mHeight/2};
        //变化前
        mPaint.setColor(Color.BLACK);
        canvas.drawLines(pts,mPaint);
        canvas.drawRect(rect,mPaint);

        canvas.translate(100,100);
        //变化后
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(rect,mPaint);
        canvas.drawLines(pts,mPaint);
    }

可以看到,整个画布都向右平移了100px然后先下平移了100px

旋转(rotate)

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

参数:
float degrees :旋转角度
float px:旋转中心横坐标
float py:旋转中心纵坐标

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        //将要绘制的矩形
        Rect rect = new Rect(0,0,200,100);
        //坐标系
        float [] pts = new float[]{0,0,mWidth/2,0,0,0,0,mHeight/2};
        //变化前
        mPaint.setColor(Color.BLACK);
        canvas.drawLines(pts,mPaint);
        canvas.drawRect(rect,mPaint);

        canvas.rotate(30);//重点:顺时针旋转画布30度
        //变化后
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(rect,mPaint);
        canvas.drawLines(pts,mPaint);
    }

默认旋转中心是原点,结果如图所示:



如果指定旋转中心

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        //将要绘制的矩形
        Rect rect = new Rect(0,0,200,100);
        //坐标系
        float [] pts = new float[]{0,0,mWidth/2,0,0,0,0,mHeight/2};
        //变化前
        mPaint.setColor(Color.BLACK);
        canvas.drawLines(pts,mPaint);
        canvas.drawRect(rect,mPaint);

        canvas.rotate(30,200,0);//重点:以(200,0)为中心顺时针旋转画布30度
        //变化后
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(rect,mPaint);
        canvas.drawLines(pts,mPaint);
    }

结果如下:

缩放(scale)

public void scale(float sx, float sy)
public final void scale(float sx, float sy, float px, float py)

参数:
float sx:水平方向伸缩的比例,假设原坐标轴的比例为n,不变时为1,在变更的X轴密度为n乘sx;所以,
sx为小数为缩小,sx为整数为放大*
float sy:垂直方向伸缩的比例,同样,小数为缩小,整数为放大
float px:旋转中心横坐标
float py:旋转中心纵坐标*
注:当缩放比例为负数时,则要根据中心轴进行翻转

废话不多说,反手就是一段代码,看看缩放到底是怎么回事:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        //将要绘制的矩形
        Rect rect = new Rect(0,0,200,100);
        //坐标系
        float [] pts = new float[]{0,0,mWidth/2,0,0,0,0,mHeight/2};
        //变化前
        mPaint.setColor(Color.BLACK);
        canvas.drawLines(pts,mPaint);
        canvas.drawRect(rect,mPaint);

        canvas.scale(0.5f,0.5f);//避免重复代码,在旋转这一小节,下面只替换这一句代码
        //变化后
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(rect,mPaint);
        canvas.drawLines(pts,mPaint);
    }

默认缩放中心是原点,结果显而易见:


我们再把缩放这一句换成

canvas.scale(0.5f,0.5f,200,100);

指定缩放中心(200,100)会怎么样呢?我们先点开android源码看看public final void scale(float sx, float sy, float px, float py)这个方法里面到底是个什么鬼

public final void scale(float sx, float sy, float px, float py) {
        translate(px, py);
        scale(sx, sy);
        translate(-px, -py);
    }

代码很清晰,先平移到缩放中心点,缩放完成后再平移回去。但这里需要注意一点:缩放以后,坐标轴比例发生变化。所以不能平移回到原点。结果如下所示


我们可以看到,画布以(200,100)为中心在横纵轴方向分别缩小了一倍
上面提到,当缩放比例为负数时需要翻转,翻转是什么意思呢?我们把旋转的这一句代码换成:

canvas.scale(-0.5f,0.5f,200,100);

看看结果如何

没错,当我们把sx系数变成负数以后,变化后的图形与原图形在x轴方向上基于原点相互对称。
最后我们总结一下:

public final void scale(float sx, float sy, float px, float py)

这个方法的变换是:先以点(px,py)为中心缩放对应的比例系数,如果比例系数是负数,然后在对应的方向上翻转(对称)。

扭曲(skew)

public void skew(float sx, float sy)

参数:
float sx:将画布在x方向上倾斜相应的角度,sx为倾斜角度的tan值,
float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.

这个方法的效果有点类似于斜拉,想了很久也没有组织好语言来描述它的效果。给个公式好了:
X = x + sx * y
Y = sy * x + y

还是直接看代码吧:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        //将要绘制的矩形
        Rect rect = new Rect(100,100,300,300);
        //坐标系
        float [] pts = new float[]{0,0,mWidth/2,0,0,0,0,mHeight/2};

        //变化前
        mPaint.setColor(Color.BLACK);
        canvas.drawLines(pts,mPaint);
        canvas.drawRect(rect,mPaint);
        //sekw
        canvas.skew((float) Math.tan(Math.PI/6),0);//sx参数为tan30度
        //变化后
        mPaint.setStrokeWidth(10);//便于看清,增加画笔宽度
        mPaint.setColor(Color.RED);
        canvas.drawRect(rect,mPaint);
        canvas.drawLines(pts,mPaint);
    }

当sx的值为Math.tan(Math.PI/6),sy为0(即Math.tan(Math.PI/2))时。因为tan90度为0。所以y轴方向上的坐标没有改变。而X轴方向上的坐标都扭曲了30度。

裁剪画布(clipXxx)

裁剪画布是利用Clip系列函数,通过与Rect、Path、Region取交、并、差等集合运算来获得最新的画布形状。除了调用Save、Restore函数以外,这个操作是不可逆的,一但Canvas画布被裁剪,就不能再被恢复!
Clip系列函数如下:
boolean clipPath(Path path)
boolean clipPath(Path path, Region.Op op)

boolean clipRect(Rect rect, Region.Op op)
boolean clipRect(RectF rect, Region.Op op)
boolean clipRect(int left, int top, int right, int bottom)
boolean clipRect(float left, float top, float right, float bottom)
boolean clipRect(RectF rect)
boolean clipRect(float left, float top, float right, float bottom, Region.Op op)
boolean clipRect(Rect rect)
boolean clipRegion(Region region)
boolean clipRegion(Region region, Region.Op op)

我们看到Region这个东西多次出现,Region的意思是“区域”,在Android里呢它同样表示的是一块封闭的区域,Region中的方法都非常的简单,我们重点来瞧瞧Region.Op,Op是Region的一个枚举类,里面呢有六个枚举常量:

图片来自https://developer.android.com/reference/android/graphics/Region.Op.html

所以Region.Op其实就是个组合模式。这个暂时不深入,后面再讲。现在只要知道Region在这里的用法和Rect是一样的
Tip:Region表示的是一个区域,而Rect表示的是一个矩形,这是最根本的区别之一。其次,Region有个很特别的地方是它不受Canvas的变换影响。
我们先把画布剪切成一个矩形试试:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        //将要绘制的矩形
        Rect rect = new Rect(0,0,200,100);

        canvas.drawColor(Color.RED);
        canvas.clipRect(rect);
        canvas.drawColor(Color.GREEN);
    }

画布剪切后,就只剩下绿色这一小块了。以后任何操作都只能在这一区域中生效。

保存(save)和回滚(restore)

前面提到除了使用save()和restore()方法,画布的操作是不可逆的,言下之意就是使用了这两个方法,可以让画布的操作可逆(已经绘制上去的图形不会发生改变)。

public int save()
public void restore()
public int getSaveCount()
public void restoreToCount(int saveCount)

Save():每次调用Save()函数,都会把当前的画布的状态进行保存,然后放入特定的栈中,该方法会返回一个int型id,代表了在画布栈中的位置,便于restoreToCount()方法回复的指定画布状态;
restore():每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
getSaveCount():返回栈中储存的画布个数
restoreToCount(int saveCount):弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复
saveLayerXxx():新建一个图层,并放入特定的栈中(性能不佳)

画布栈这个概念我们一图以蔽之:

图片来自http://blog.csdn.net/linghu_java/article/details/8939952

先来看一下最简单的save()和restore():

canvas.save()

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);
        //坐标系
        float [] pts = new float[]{0,0,mWidth/2,0,0,0,0,mHeight/2};
        mPaint.setColor(Color.BLACK);
        canvas.drawLines(pts,mPaint);

        RectF rect = new RectF(50, 50, 300, 200);

        mPaint.setColor(Color.RED);
        canvas.drawRect(rect, mPaint);

        // 保存画布----(接下来的代码只替换这一句)
        canvas.save();
        // 向下平移画布
        canvas.translate(0,250);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(rect, mPaint);

        // 还原画布
        canvas.restore();

        mPaint.setColor(Color.GREEN);
        //绘制椭圆
        canvas.drawOval(rect, mPaint);
    }

我们先绘制了一个红色矩形,在save()后添加向下平移,然后绘制蓝色矩形,绘制后释放了画布,最后绘制绿色椭圆。你会发现平移的只有蓝色的矩形,之后绘制的绿色椭圆没有平移。这就说明蓝色的矩形被我们放在了一个新的层上。一旦你回滚(restore)了画布,就又回到原来的画布状态。所以我们的绿色椭圆没有发生任何变化。

canvas.saveLayer()

接下来我们把

 canvas.save();

替换成

canvas.saveLayer(0, 0, 400, 400, null, Canvas.ALL_SAVE_FLAG);

前面4个参数是控制保存范围的,因为saveLayerXXX方法会将操作保存到一个新的Bitmap中,而这个Bitmap的大小取决于我们传入的参数大小,Bitmap是个相当危险的对象,很多朋友在操作Bitmap时不太理解其原理经常导致OOM,在saveLayer时我们会依据传入的参数获取一个相同大小的Bitmap,虽然这个Bitmap是空的但是其会占用一定的内存空间,我们希望尽可能小地保存该保存的区域,而saveLayer则提供了这样的功能。

替换后得到的结果如下:


由于我们只保存了(0,0,400,400)这一个区域画布,所以超出的部分自然无法绘制。

canvas.saveLayerAlpha()

继续把

 canvas.save();

替换成

canvas.saveLayerAlpha(0, 0, 400, 400, 50, Canvas.ALL_SAVE_FLAG);

很清楚的可以看到,该方法可以在我们保存画布时设置画布的透明度:


flag

我们在用saveLayerAlpha和saveLayer方法时都用到了一个flag值Canvas.ALL_SAVE_FLAG,这是什么鬼?看一下官网怎么说:


整理一下

数据类型 名称 简介
int ALL_SAVE_FLAG 默认,保存全部状态
int CLIP_SAVE_FLAG 保存剪辑区
int CLIP_TO_LAYER_SAVE_FLAG 剪裁区作为图层保存
int FULL_COLOR_LAYER_SAVE_FLAG 保存图层的全部色彩通道
int HAS_ALPHA_LAYER_SAVE_FLAG 保存图层的alpha(不透明度)通道
int MATRIX_SAVE_FLAG 保存Matrix信息(translate, rotate, scale, skew)

这六个常量值分别标识了我们保存什么东西。
六个标识位除了
CLIP_SAVE_FLAG MATRIX_SAVE_FLAG ALL_SAVE_FLAG
savesaveLayerXXX方法都通用外,其余三个只能使saveLayerXXX方法有效。也就是说
CLIP_SAVE_FLAG MATRIX_SAVE_FLAG ALL_SAVE_FLAG
可以被save(int flag)和saveLayerXXX(…,int flag)使用,其余的仅仅适用于saveLayerXXX方法。平时使用大家可以直接ALL_SAVE_FLAG就行,感兴趣的可以了解下别的标志位的作用。
在此简单举个例子:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        RectF rect = new RectF(0,0,200,100);
        mPaint.setColor(Color.RED);
        canvas.drawRect(rect,mPaint);//先绘制一个红色矩形

        canvas.save(Canvas.CLIP_SAVE_FLAG);//只保存clip操作
        canvas.translate(100,100);//平移(MATRIX操作)
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(rect,mPaint);//绘制一个绿色矩形
        canvas.restore();//回滚

        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(10);
        canvas.drawOval(rect,mPaint);//绘制一个黑色椭圆
    }

这里我们先绘制了一个红色矩形,在save()的时候传入了一个参数:Canvas.CLIP_SAVE_FLAG。即只保存剪辑区(clip操作)。save后我们将画布平移,绘制了一个绿色矩形,然后回滚,最后绘制了一个黑色椭圆

看看结果如何:


可以看到,虽然我们回滚后才绘制的黑色椭圆,但是椭圆依然"岿然不动"。很明显,这是因为我们只保存了clip操作(剪辑区)。所以MATRIX操作的部分不会回滚。

canvas.restoreToCount()

这东西事实上没什么好讲的,和restore()的功能一样,区别是弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复。

canvas.getSaveCount()

该方法返回画布栈中保存画布的次数,但是有时系统会调用,所以即使没有调用save()。getSaveCount()返回的值也不一定是1(最小值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。)。故想要回滚到指定画布状态,最好在调用save()方法时记录saveID,然后调用restoreToCount()方法回滚到指定画布状态。

小练习走一波,利用save()和restore()绘制出如下图形:


代码:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth()/2,getHeight()/2);

        for(int i = 0;i < 36;i++){
            canvas.save();
            canvas.rotate(i*10);
            canvas.drawLine(0,0,300,0,mPaint);
            canvas.restore();
        }
    }

concat();

public void concat(Matrix matrix)

参数:matrix
使整个画布做matrix变形操作

总结

其中漏讲了一些内容,后面择机补上

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容