Android自定义控件进阶——Canvas之画布操作

一.Canvas的常用操作速查表

操作类型 相关API 备注
绘制颜色 drawColor,drawRGB,drawARGB 使用单一颜色填充整个画布
绘制基本形状 drawPoint,drawPoints,drawLine,drawLines,drawRect,drawRoundRect,drawOval,drawCircle,drawArc 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
绘制图片 drawBitmap,drawPicture 绘制位图和图片
绘制文本 drawText,drawPosText,drawTextOnPath 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
绘制路径 drawPath 绘制路径,绘制贝塞尔曲线时也需要用到该函数
顶点操作 drawVertices,drawBitmapMesh 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用
画布剪裁 clipPath,clipRect 设置画布的显示区域
画布快照 save,restore,saveLayerXxx,restoreToCount,getSaveCount 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
画布变换 translate,scale,rotate,skew 依次为 位移、缩放、 旋转、错切
Matrix(矩阵) getMatrix,setMatrix,concat 实际上画布的位移,缩放等操作的都是图像矩阵Matrix, 只不过Matrix比较难以理解和使用,故封装了一些常用的方法。

二.Canvas基本操作

1.画布操作

为什么要有画布操作?

画布操作可以帮助我们用更加容易理解的方式制作图形。

例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?

按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。

然而这是否是被固有思维禁锢了?

假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。


⑴位移(translate)

translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动,如下:

// 省略了创建画笔的代码

// 在坐标原点绘制一个黑色圆形
mPaint.setColor(Color.BLACK);
canvas.translate(200,200);
canvas.drawCircle(0,100,mPaint);

// 在坐标原点绘制一个蓝色圆形
mPaint.setColor(Color.BLUE);
canvas.translate(200,mPaint);

我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的


⑵缩放(scale)

缩放提供了两个方法,如下:

public void scale (float sx,float sy)

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

这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。

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

取值范围(n) 说明
(-∞,-1) 先根据缩放中心放大n倍,再根据中心轴进行翻转
-1 根据缩放中心轴进行翻转
(-1,0) 先根据缩放中心缩小到n,再根据中心轴进行翻转
0 不会显示,若sx为0,则宽度为0,不会显示,sy同理
(0,1) 根据缩放中心缩小到n
1 没有变化
(1,+∞) 根据缩放中心放大n倍

如果在缩放时稍微注意一下就会发现缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴,如下:

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.scale(0.5f,0.5f);                // 画布缩放

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

(为了更加直观,我添加了一个坐标系,可以比较明显的看出,缩放中心就是坐标原点)

接下来我们使用第二种方法让缩放中心位置稍微改变一下,如下:

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

(图中用箭头指示的就是缩放中心。)

前面两个示例缩放的数值都是正数,按照表格中的说明,当缩放比例为负数的时候会根据缩放中心轴进行翻转,下面我们就来实验一下:

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,mPaint);


canvas.scale(-0.5f,-0.5f);          // 画布缩放

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的。

由于本次未对缩放中心进行偏移,所有默认的缩放中心就是坐标原点,中心轴就是x轴和y轴。

本次缩放可以看做是先根据缩放中心(坐标原点)缩放到原来的0.5倍,然后分别按照x轴和y轴进行翻转。

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,mPaint);

canvas.scale(-0.5f,-0.5f,mPaint);

添加了这么多的辅助内容,希望大家能够看懂。

本次对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了。

PS:和位移(translate)一样,缩放也是可以叠加的。

canvas.scale(0.5f,0.5f);
canvas.scale(0.5f,0.1f);

调用两次缩放则 x轴实际缩放为0.5x0.5=0.25 y轴实际缩放为0.5x0.1=0.05

下面我们利用这一特性制作一个有趣的图形。

注意设置画笔模式为描边(STROKE)

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,mHeight / 2);

RectF rect = new RectF(-400,400);   // 矩形区域

for (int i=0; i<=20; i++)
{
	canvas.scale(0.9f,0.9f);
	canvas.drawRect(rect,mPaint);
}


⑶旋转(rotate)

旋转提供了两种方法:

public void rotate (float degrees)

public final void rotate (float degrees,float py)

和缩放一样,第二种方法多出来的两个参数依旧是控制旋转中心点的。

默认的旋转中心依旧是坐标原点:

public void rotate (float degrees)

public final void rotate (float degrees,float py)

改变旋转中心位置:

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,mPaint);

canvas.rotate(180,0);               // 旋转180度 <-- 旋转中心向右偏移200个单位

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

好吧,旋转也是可叠加的

canvas.rotate(180);
canvas.rotate(20);

调用两次旋转,则实际的旋转角度为180+20=200度。

为了演示这一个效果,我做了一个不明觉厉的东西:

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,mHeight / 2);

canvas.drawCircle(0,mPaint);          // 绘制两个圆形
canvas.drawCircle(0,380,mPaint);

for (int i=0; i<=360; i+=10){               // 绘制圆形之间的连接线
   canvas.drawLine(0,mPaint);
   canvas.rotate(10);
}


⑷错切(skew)

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

示例:

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,200);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.skew(1,0);                       // 水平错切 <- 45度

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

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

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2,0);                       // 水平错切
canvas.skew(0,1);                       // 垂直错切

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);


⑸快照(save)和回滚(restore)

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

与之相关的API:

相关API 简介
save 把当前的画布的状态进行保存,然后放入特定的栈中
saveLayerXxx 新建一个图层,并放入特定的栈中
restore 把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布
restoreToCount 弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复
getSaveCount 获取栈中内容的数量(即保存次数)

下面对其中的一些概念和方法进行分析:

状态栈:

其实这个栈我也不知道叫什么名字,暂时叫做状态栈吧,它看起来像下面这样:

这个栈可以存储画布状态和图层状态。

Q:什么是画布和图层? A:实际上我们看到的画布是由多个图层构成的,如下图(图片来自网络):

实际上我们之前讲解的绘制操作和画布操作都是在默认图层上进行的。 在通常情况下,使用默认图层就可满足需求,但是如果需要绘制比较复杂的内容,如地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)等,则分图层绘制比较好一些。 你可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。

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

save 有两种方法:

// 保存全部状态
public int save ()

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

可以看到第二种方法比第一种多了一个saveFlags参数,使用这个参数可以只保存一部分状态,更加灵活,这个saveFlags参数具体可参考上面表格中的内容。

每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。

saveLayerXxx

saveLayerXxx有比较多的方法:

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

// 有图层alpha(不透明度)通道
public int saveLayerAlpha (RectF bounds,int alpha)
public int saveLayerAlpha (RectF bounds,int alpha,int saveFlags)
public int saveLayerAlpha (float left,int alpha)
public int saveLayerAlpha (float left,int saveFlags)

注意:saveLayerXxx方法会让你花费更多的时间去渲染图像(图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。

使用saveLayerXxx方法,也会将图层状态也放入状态栈中,同样使用restore方法进行恢复。

这个暂时不过多讲述,如果以后用到详细讲解。(因为这里面东西也有不少啊QAQ)

restore

状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。

同样以上面状态栈图片为例,调用一次restore方法则将状态栈中第5次取出,根据里面保存的状态进行状态恢复。

restoreToCount

弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。

以上面状态栈图片为例,如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。

getSaveCount

获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。

不过请注意,该函数的最小返回值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。

常用格式

虽然关于状态的保存和回滚啰嗦了不少,不过大多数情况下只需要记住下面的步骤就可以了:

save();      //保存状态
...          //具体操作
restore();   //回滚到之前的状态

这种方式也是最简单和最容易理解的使用方法。


三.总结

如本文一开始所说,合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果。

(,• ₃ •,)

PS: 由于本人英文水平有限,某些地方可能存在误解或词语翻译不准确,如果你对此有疑问可以提交Issues进行反馈。

更多Android 知识点可参考

Android 性能调优系列https://0a.fit/dNHYY

Android 车载学习指南https://0a.fit/jdVoy

Android Framework核心知识点笔记https://0a.fit/acnLL

Android 八大知识体系https://0a.fit/mieWJ

Android 中高级面试题锦https://0a.fit/YXwVq

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具