有大佬指引着方向,才能顺着明亮的道路走的更远。核心的知识都在扔物线大佬的HenCoder中有写,而且真心赞!为了避免给大佬和阅读这篇文章的同学造成不适,就不再过多引用大佬文章中的内容了。
Canvas既然是画布,自然先从“画”看起。
基本绘制drawXXX系列
颜色
-
drawARGB
-
drawColor
-
drawRGB
基础图形
-
drawArc
-
drawCircle
-
drawDoubleRoundRect
绘制双框圆角矩形
-
drawLine
-
drawOval
-
drawPoint
-
drawRect
-
drawRoundRect
文字
-
drawPosText
可以分别指定每个文字的位置
-
drawText
-
drawTextOnPath
根据路径绘制文字
-
drawTextRun
用于辅助一些文字结构比较特殊的语言的绘制,例如阿拉伯文字
Bitmap
-
drawBitmap(bitmap,left,top,paint)
指定bitmap的左边界和上边界,然后绘制bitmap
-
drawBitmap(bitmap,srcRect,dstRect,paint)
用srcRect去切割bitmap指定区域内容,dst指定绘制到canvas的区域边界。
-
drawBitmap(bitmap,matrix,paint)
绘制bitmap时,将matrix设置的几何变换应用上。
-
drawBitmapMesh
将整个Bitmap分成若干个网格,再对每一个网格进行相应的扭曲处理。可实现水波纹效果。
特殊
-
drawPaint
用画笔当前的配置,填充画布。
-
drawPath
-
将一个复杂的绘制场景进行拆分,可以进行部分重绘。
-
绘制3D图形利器
更多详细内容,请移步Hencoder-绘制基础,香!
对绘制的辅助
裁切
将canvas裁剪成规定的形状,然后再绘制的内容就在这个裁剪区域内了。
-
clipOutPath
-
clipOutRect
-
clipPath
-
clipRect
几何变换
-
translate
-
rotate
-
scale
-
skew
-
setMatrix
用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换。
-
concat(matrix)
用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。
使用Camera做三维变换
详细内容,请移步HenCoder-Canvas对绘制的辅助,香!
绘制过程中需多次进行几何变换时
需要注意,如果绘制过程需要对canvas进行多次的几何变换,那么需要倒叙来写几何变换过程。比如需要先平移再旋转,那么在写代码的时候,就需要先旋转再平移。
这里主要是因为屏幕的坐标系和canvas坐标系是两个坐标系,需要进行一定的的空间想象。
当然,也可以初始化一个Matrix,合理的使用preXXX和postXXX,对该Matrix进行几何变换操作,然后将其应用到canvas上。
View的绘制顺序
详细内容,请移步HenCoder-自定义View绘制顺序,这里就只引用两张大佬文章中的图片镇楼:
Canvas的回退栈
当我们在使用canvas的辅助函数,对canvas进行操作时,这些操作都是不可逆的。比如,在绘制某个内容之前,使用clipRect(0,0,100,100),那么之后的绘制就只能在[0,0,100,100]这个矩形内,除非在通过手动调用api,让canvas回到之前的某个状态。
save和restore
为了避免发生这种情况,就可以在特定的位置进行保存和恢复,在进行变换前,使用save保存canvas当前的状态,然后进行变换,接着绘制我们要绘制的内容,最后再通过restore恢复之前保存的状态。
如果在一次绘制中,多次调用save方法,那么会将每次save时,canvas的状态压入类似一个栈中,每一个状态都对应一个数字,代表其是栈中的第几个,可以通过方法restoreToCount(count),将canvas回退到指定的那个。也可以调用restore,一个一个的回退canvas的状态。
需要注意的是,不管是调用restore还是restoreToCount,都需要在save的数量范围内,否者系统就会抛出异常。
Canvas.drawXXX工作过程
当我们使用canvas.drawXXX时,系统会在一个新的透明区域,绘制我们要绘制的内容,然后迅速与屏幕当前显示内容进行重叠,这个重叠的过程也会受xfermode或blendmode的影响。如下示例,就演示了这个情况:
不设置xfermode:
override fun onDraw(canvas: Canvas) {
//先将背景涂红
canvas.drawColor(Color.RED)
//在中心画一个绿色的圆
paint.color = Color.GREEN
canvas.drawCircle(width/2f,height/2f,radios/2f,paint)
}
复制代码
得到的结果是这样的:
设置xfermode为DST:
override fun onDraw(canvas: Canvas) {
//先将背景涂红
canvas.drawColor(Color.RED)
//在中心画一个绿色的圆
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST)
paint.color = Color.GREEN
canvas.drawCircle(width/2f,height/2f,radios/2f,paint)
paint.xfermode = null
}
复制代码
得到的结果是这样的:
如果在绘制过程中只是给paint设置xfermode,而没有进行操作,如保存Canvas状态信息等,那么:
-
如果设置的mode需要削掉DST(即已经在屏幕上显示的)部分或全部内容,那么这个mode不会生效
-
如果设置的mode为SRC_OUT、DST_OUT或XOR时,那么SRC区域显示为黑色,再覆盖在已显示的内容上
使用layer综合绘制操作
既然只是直接使用paint.setXfermode设置的效果,会跟预期的不一致,那么应该怎么样才能获得预期的效果呢?
canvas提供了saveLayer方法,抽取一个透明区域,执行绘制方法,随后再一并将绘制的内容,覆盖在已显示内容上,使用和不使用saveLayer的大致工作流程:
- 不使用layer
- 使用layer
在调用saveLayer时,可以传入一个saveFlags参数,它有如下几个参数可以设置:
-
MATRIX_SAVE_FLAG只保存图层的matrix矩阵
-
CLIP_SAVE_FLAG只保存大小信息
-
HAS_ALPHA_LAYER_SAVE_FLAG表明该图层有透明度,和下面的标识冲突,都设置时以下面的标志为准
-
FULL_COLOR_LAYER_SAVE_FLAG完全保留该图层颜色(和上一图层合并时,清空上一图层的重叠区域,保留该图层的颜色)
-
CLIP_TO_LAYER_SAVE_FLAG创建图层时,会把canvas(所有图层)裁剪到参数指定的范围,如果省略这个flag将导致图层开销巨大(实际上图层没有裁剪,与原图层一样大)
-
ALL_SAVE_FLAG
保存所有信息
再来看一下canvas.save()源码:
public int save() {
return nSave(mNativeCanvasWrapper,
//保留矩阵信息(记录了canvas位移、缩放、旋转情况)
//保留clipXXX信息(clipRect、clipPath等)
//其他信息不保留,如已绘制内容信息等
MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
}
复制代码
硬件加速
从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道支持硬件加速,也就是说,在 View 的画布上执行的所有绘制操作都会使用 GPU。启用硬件加速需要更多资源,因此应用会占用更多内存。如果最低 版本为 14 及更高级别,则硬件加速默认处于启用状态。
什么是硬件加速
所谓硬件加速,指的是把某些计算工作交给专门的硬件来做,而不是和普通的计算工作一样交给 CPU 来处理。这样不仅减轻了 CPU 的压力,而且由于有了「专人」的处理,这份计算工作的速度也被加快了。这就是「硬件加速」。
而对于 Android 来说,硬件加速有它专属的意思:在 Android 里,硬件加速专指把 View 中绘制的计算工作交给 GPU 来处理。进一步地再明确一下,这个「绘制的计算工作」指的就是把绘制方法中的那些 Canvas.drawx3X() 变成实际的像素这件事。
怎么就加速了
-
用了 GPU(自身的设计本来就对于很多常见类型内容的计算,例如简单的圆形、简单的方形等具有优势),绘制变快了
-
绘制机制的改变,导致界面内容改变时的刷新效率极大提高
如果想了解更多关于硬件加速的底层原理,可以查看这篇文章——理解Android硬件加速原理的小白文。
硬件加速开关
-
Application
<application android:hardwareAccelerated="true" ...> 复制代码
-
Activity
<application android:hardwareAccelerated="true"> <activity ... /> <activity android:hardwareAccelerated="false" /> </application> 复制代码
-
Window
//window层级,只能开启,无法进行关闭 window.setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED ) 复制代码
-
View
//控制当前view图层是否使用硬件加速,如果应用总体未开启硬件加速,那么即便设置type为LAYER_TYPE_HARDWARE,也无法开启硬件加速 myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null) 复制代码
-
程序运行中
1.view.isHardwareAccelerated(),判断当前是否开启硬件加速
2.canvas.isHardwareAccelerated(),判断当前是否开启硬件加速
由于通常情况下,canvas是绘制的载体,所以应该通过canvas进行判断
硬件加速使用限制
硬件加速虽好,但开启硬件加速后,绘制Canvas的某些方法失效或无效,在使用时需要注意这些方法:
-
Canvas
方法 开始支持API版本 drawBitmapMesh()(颜色数组) 18 drawPicture() 23 drawPosText() 16 drawTextOnPath() 16 drawVertices() ✗ setDrawFilter() 16 clipPath() 18 clipRegion() 18 clipRect(Region.Op.XOR) 18 clipRect(Region.Op.Difference) 18 clipRect(Region.Op.ReverseDifference) 18 clipRect()(通过旋转/透视) 18 -
Paint
方法 开始支持API版本 setAntiAlias()(适用于文本)(颜色数组) 18 setAntiAlias()(适用于线条) 16 setFilterBitmap() 17 setLinearText() ✗ setMaskFilter() ✗ setPathEffect()(适用于线条) 28 setShadowLayer()(除文本之外) 28 setStrokeCap()(适用于线条) 18 setStrokeCap()(适用于点) 19 setSubpixelText() 28 -
Xfermode
方法 开始支持API版本 PorterDuff.Mode.DARKEN(帧缓冲区) 28 PorterDuff.Mode.LIGHTEN(帧缓冲区) 28 PorterDuff.Mode.OVERLAY(帧缓冲区) 28 -
Shader
方法 开始支持API版本 ComposeShader 内的 ComposeShader 28 ComposeShader 内相同类型的着色器) 26 ComposeShader 上的本地矩阵 18 -
Scale
方法 开始支持API版本 drawText() 18 drawPosText() 28 drawTextOnPath() 28 简单的形状 17 复杂的形状 28 drawPath() 28 阴影层 28 简单形状,是指使用 Paint 发出的 drawRect()、drawCircle()、drawOval()、drawRoundRect() 和 drawArc()(其中 useCenter=false)命令,该 Paint 不包含 PathEffect,也不包含非默认联接(通过 setStrokeJoin()/setStrokeMiter())。这些绘制命令的其他实例都属于上表中的“复杂”形状。
离屏缓冲 —— 引用自Hencoder
前面说到了,在view中进行的操作,如果不被硬件加速支持,那么就需要适时的关闭硬件加速:
view.setLayerType(LAYER_TYPE_SOFTWARE, null);
复制代码
但是这个方法的本意是设置view layer的类型,如果类型设置为LAYER_TYPE_SOFTWARE,那么会顺便关闭硬件加速。
所谓 View Layer,又称为离屏缓冲(Off-screen Buffer),它的作用是单独启用一块地方来绘制这个 View ,而不是使用软件绘制的 Bitmap 或者通过硬件加速的 GPU。这块「地方」可能是一块单独的 Bitmap,也可能是一块 OpenGL 的纹理(texture,OpenGL 的纹理可以简单理解为图像的意思),具体取决于硬件加速是否开启。
采用什么来绘制 View 不是关键,关键在于当设置了 View Layer 的时候,它的绘制会被缓存下来,而且缓存的是最终的绘制结果,而不是像硬件加速那样只是把 GPU 的操作保存下来再交给 GPU 去计算。通过这样更进一步的缓存方式,View 的重绘效率进一步提高了:只要绘制的内容没有变,那么不论是 CPU 绘制还是 GPU 绘制,它们都不用重新计算,而只要只用之前缓存的绘制结果就可以了。
基于这样的原理,在进行移动、旋转等(无需调用 invalidate())的属性动画的时候开启 Hardware Layer 将会极大地提升动画的效率,因为在动画过程中 View 本身并没有发生改变,只是它的位置或角度改变了,而这种改变是可以由 GPU 通过简单计算就完成的,并不需要重绘整个 View。所以在这种动画的过程中开启 Hardware Layer,可以让本来就依靠硬件加速而变流畅了的动画变得更加流畅。
view.setLayerType(LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(LAYER_TYPE_NONE, null);
}
});
animator.start();
或者
view.animate()
.rotationY(90)
.withLayer(); // withLayer() 可以自动完成上面这段代码的复杂操作
复制代码
不过一定要注意,只有你在对 translationX translationY rotation alpha scale等无需调用 invalidate() 的属性做动画的时候,这种方法才适用,因为这种方法本身利用的就是当界面不发生时,缓存未更新所带来的时间的节省。
使用硬件加速的提示和技巧
-
减少应用中的视图数量
系统需要绘制的视图越多,运行速度越慢。这也适用于软件渲染。减少视图是优化界面最简单的方法之一。
-
避免过度绘制
请勿在彼此上方绘制过多层。移除所有被上方的其他不透明视图完全遮挡的视图。如果需要在彼此上方混合绘制多个层,请考虑将它们合并为一个层。
-
不要在绘制方法中创建渲染对象
一个常见的错误是,每次调用渲染方法时都创建新的 Paint 或 Path。这会强制GC更频繁地运行,同时还会绕过硬件管道中的缓存和优化。
-
不要过于频繁地修改形状
例如,使用纹理遮罩渲染复杂的形状、路径和圆圈。每次创建或修改路径时,硬件管道都会创建新的遮罩,成本可能比较高。
-
不要过于频繁地修改位图
每次更改位图的内容时,系统都会在下次绘制时将其作为 GPU 纹理再次上传。
-
谨慎使用 Alpha
当使用 setAlpha()、AlphaAnimation 或 ObjectAnimator 将视图设置为半透明时,该视图会在屏幕外缓冲区渲染,导致所需的填充率翻倍。在超大视图上应用 Alpha 时,请考虑将视图的层类型设置为 LAYER_TYPE_HARDWARE。
BitmapShader可能引起崩溃
Canvas的父类是BaseCanvas,canvas的一系列drawXXX行为,都是调用的super.drawXXX,在BaseCanvas中,各个drawXXX都会对paint的shader进行一个校验:
private void throwIfHasHwBitmapInSwMode(Paint p) {
if (isHardwareAccelerated() || p == null) {
return;
}
//如果当前canvas没有使用硬件加速,那么会进入这个方法
throwIfHasHwBitmapInSwMode(p.getShader());
}
private void throwIfHasHwBitmapInSwMode(Shader shader) {
if (shader == null) {
return;
}
if (shader instanceof BitmapShader) {
//如果paint设置了shaer且为BitmapShader,那么再进一步判断
throwIfHwBitmapInSwMode(((BitmapShader) shader).mBitmap);
}
if (shader instanceof ComposeShader) {
throwIfHasHwBitmapInSwMode(((ComposeShader) shader).mShaderA);
throwIfHasHwBitmapInSwMode(((ComposeShader) shader).mShaderB);
}
}
private void throwIfHwBitmapInSwMode(Bitmap bitmap) {
if (!isHardwareAccelerated() && bitmap.getConfig() == Bitmap.Config.HARDWARE) {
//当前没有启用硬件加速,但是bitmap需要硬件加速
onHwBitmapInSwMode();
}
}
protected void onHwBitmapInSwMode() {
//检验是否允许硬件加速bitmap存在于非硬件加速的环境绘制,如果不允许,那么会直接抛出异常
if (!mAllowHwBitmapsInSwMode) {
throw new IllegalArgumentException(
"Software rendering doesn't support hardware bitmaps");
}
}
//这个属性默认为false
private boolean mAllowHwBitmapsInSwMode = false;
//setter方法也被hide标记
/**
* @hide
*/
public void setHwBitmapsInSwModeEnabled(boolean enabled) {
mAllowHwBitmapsInSwMode = enabled;
}
复制代码
那么在给Paint设置shader时,如果是BitmapShader一定要注意,Config是否为Bitmap.Config.HARDWARE类型。