阅读 1395

Android必知必会——Canvas

有大佬指引着方向,才能顺着明亮的道路走的更远。核心的知识都在扔物线大佬的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

  • drawPicture

  • drawRenderNode

    将一个复杂的绘制场景进行拆分,可以进行部分重绘。

  • drawVertices

    绘制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类型。