程序员人生 网站导航

[置顶] 【腾讯bugly干货分享】Android自绘动画实现与优化实战——以Tencent OS录音机波形动

栏目:综合技术时间:2016-08-26 09:25:30

本文来自于腾讯bugly社区,原文地址为:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1180

前言

我们所熟知的,Android 的图形绘制主要是基于 View 这个类实现。 每一个 View 的绘制都需要经过 onMeasure、onLayout、onDraw 3步曲,分别对应到丈量大小、布局、绘制。

Android 系统为了简化线程开发,下降利用开发的难度,将这3个进程都放在利用的主线程(UI 线程)中履行,以保证绘制系统的线程安全。

这3个进程通过1个叫 Choreographer 的定时器来驱动调用更新, Choreographer 每16ms被 vsync 这个信号唤醒调用1次,这有点类似初期的电视机刷新的机制。在 Choreographer 的 doFrame 方法中,通过树状结构存储的 ViewGroup,顺次递归的调用到每一个 View 的 onMeasure、onLayout、onDraw 方法,从而最后将每一个 View 都绘制出来(固然最后还会经过 SurfaceFlinger 的类来将 View 合成起来显示,实际进程很复杂)。

同时每一个 View 都保存了很多标记值 flag,用来判断是不是该 View 需要重新被 Measure、Layout、Draw。 这样对那些没有变化,不需要重绘的 View,则不再调用它们的方法,从而能够提高绘制效力。

Android 为了方便开发者进行动画开发,提供了好几种动画实现的方式。 其中比较经常使用的是属性动画类(ObjectAnimator),它通过定时以1定的曲线速率来改变 View 的1系列属性,最后产生 View 的动画的效果。比较常见的属性动画能够动态的改变 View 的大小、色彩、透明度、位置等值,此种方式实现的效力比较高,也是官方推荐的动画情势。

为了进1步的提升动画的效力,避免每次都需要屡次调用 onMeasure、onLayout、onDraw,重新绘制 View 本身。 Android 还提出了1个层 Layer 的概念。

通过将 View 保存在图层中,对平移、旋转、伸缩等动画,只需要对该层进行整体变化,而不再需要重新绘制 View 本身。 层 Layer 又分为软绘层(Software Layer)和硬绘层(Harderware Layer) 。它们可以通过 View 类的 setLayerType(layerType, paint);方法进行设置。软绘层将 View 存储成 bitmap,它会占用普通内存;而硬绘层则将 View 存储成纹理(Texture),占用 GPU 中的存储。 需要注意的是,由于将 View 保存在图层中,都会占用相应的内存,因此在动画结束以后需要重新设置成LAYER_ TYPE_ NONE,释放内存。

由于普通的 View 都处于主线程中,Android 除绘制以外,在主线程中还需要处理用户的各种点击事件。很多情况,在主线程中还需要运行额外的用户处理逻辑、轮询消息事件等。 如果主线程过于繁忙,不能及时的处理和响利用户的输入,会让用户的体验急剧下降。如果更严重的情况,当主线程延迟时间到达5s的时候,还会触发 ANR(Application Not Responding)。 这样当界面的绘制和动画比较复杂,计算量比较大的情况,就不再合适使用 View 这类方式来绘制了。

Android 斟酌到这类场景,提出了 SurfaceView 的机制。SurfaceView 能够在非 UI 线程中进行图形绘制,释放了 UI 线程的压力。SurfaceView 的使用方法1般是复写1下3种方法:

   public void surfaceCreated(SurfaceHolder holder);
   public void surfaceChanged(SurfaceHolder holder, int format, int width,
                              int height);
   public void surfaceDestroyed(SurfaceHolder holder);

surfaceCreated 在 SurfaceView 被创建的时候调用, 1般在该方法中创建绘制线程,并启动这个线程。

surfaceDestroyed 在 SurfaceView 被烧毁的时候调用,在该方法中设置标记位,让绘制线程停止运行。

绘制子线程中,1般是1个 while 循环,通过判断标记位来决定是不是退出该子线程。 使用 sleep 函数来定时的调起绘制逻辑。 通过 mHolder.lockCanvas()来取得 canvas,绘制终了以后调用 mHolder.unlockCanvasAndPost(canvas);来上屏。 这里特别要注意绘制线程和 surfaceDestroyed 中需要加锁。否则会有 SurfaceView 被烧毁了,但是绘制子线程中还是持有对 Canvas 的援用,而致使 crash。下面是1个经常使用的框架:

private final Object mSurfaceLock = new Object();
private DrawThread mThread;
@Override
public void surfaceCreated(SurfaceHolder holder) {
    mThread = new DrawThread(holder);
    mThread.setRun(true);  
    mThread.start();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
                           int height) {
    //这里可以获得SurfaceView的宽高等信息
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    synchronized (mSurfaceLock) {  //这里需要加锁,否则doDraw中有可能会crash
        mThread.setRun(false);
    }
}

private class DrawThread extends Thread {
    private SurfaceHolder mHolder;
    private boolean mIsRun = false;

    public DrawThread(SurfaceHolder holder) {
        super(TAG);
        mHolder = holder;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (mSurfaceLock) {
                if (!mIsRun) {
                    return;
                }
                Canvas canvas = mHolder.lockCanvas();
                if (canvas != null) {
                    doDraw(canvas);  //这里做真正绘制的事情
                    mHolder.unlockCanvasAndPost(canvas);
                }
            }
            Thread.sleep(SLEEP_TIME);
        }
    }

    public void setRun(boolean isRun) {
        this.mIsRun = isRun;
    }
}

Android 为绘制图形提供了 Canvas 类,可以理解这个类是1块画布,它提供了在画布上画不同图形的方法。它提供了1系列的绘制各种图形的 API, 比如绘制矩形、圆形、椭圆等。对应的 API 都是 drawXXX的情势。

不规则的图形的绘制比较特殊,它同于规则图形已有绘制公式的情况,它有多是任意的线条组成。Canvas 为画不规则形状,提供了 Path 这个类。通过 Path 能够记录各种轨迹,它可以是点、线、各种形状的组合。通过 drawPath 这个方法便可绘制出任意图形。

有了画布 Canvas 类,提供了绘制各种图形的工具以后,还需要指定画笔的色彩,样式等属性,才能有效的绘图。Android 提供了 Paint 这个类,来抽象画笔。 通过 Paint 可以指定绘制的色彩,是不是填充,如果处理交集等属性。

动画实现

既然是实战,固然要有1个例子啦。 这里以 TOS 里面的录音机的波形动效实现为例。 首先看1下设计狮童鞋给的视觉设计图:

下面是动起来的效果图:

看到这么高大上的动效图,不能不赞叹1下设计狮童鞋,但同时也深深的捏了把汗——这个动画要咋实现捏。

粗略的看1下上面的视觉图。 感觉像是多个正弦曲线组成。 每条正弦线好像中间高,两边低,应当有1个对称的衰减系数。 同时有两组上下对称的正弦线,在对称的正弦线中间采取渐变色彩来进行填充。然后看动效的效果图,好像这个不规则的正弦曲线有1个固定的速率向前在运动。

看来为了实现这个动效图,还得把都已还给老师的那点可怜的数学知识捡起来。下面是正弦曲线的公式:

y=Asin(ωx+φ)+k

A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离;ω 是角速度,换成频率是 2πf,能够控制波形的宽度;φ 是初始相位,能够决定正弦曲线的初始 x 轴位置;k 是偏距,能够控制在 y 轴上的偏移量

为了能够更加直观,将公式图形化的显示出来,这里强烈推荐1个网站:https://www.desmos.com/calculator ,它能将输入的公式转换成坐标图。这正是我们需要的。比如 sin(0.75πx - 0.5π) 对应的图形是下图:

与上面设计图中的相比,还需要乘上1个对称的衰减函数。 我们挑选了以下的衰减函数425/(4+x4):

将sin(0.75πx - 0.5π) 乘以这个衰减函数 425/(4+x4),然后乘以0.5。 最后得出了下图:

看起来这个曲线与视觉图中的曲线已很像了,不过就是多加几个算法类似,但是相位不同的曲线罢了。 以下图:

看看,用了我们足(quan)够(bu)强(wang)大(ji)的数学知识以后, 我们好像也创造出来了类似视觉稿中的波形了。

接下来,我们只需要在 SurfaceView 中使用 Path,通过上面的公式计算出1个个的点,然后画直线连接起来就行啦! 因而我们得出了下面的实际效果(为了方便显示,已将背景调成白色):

曲线画出来了,然后要做的就是渐变色的填充了。 这也是视觉还原比较难实现的地方。

对渐变填充,Android 提供了 LinearGradient 这个类。它需要提供起始点和终结点的坐标,和起始点和终结点的色彩值:

public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,
             TileMode tile);

TileMode 包括了 CLAMP、REPEAT、MIRROR 3种模式。 它指定了,如果填充的区域超过了起始点和终结点的距离,色彩重复的模式。CLAMP 指使用终点边沿的色彩,REPEAT 指重复的渐变,而MIRROR则指的是镜像重复。

从 LinearGradient 的构造函数就能够预知,渐变填充的时候,1定要指定精确的起始点和终结点。否则如果渐变距离大于填充区域,会出现渐变不完全,而渐变距离小于填充区域则会出现多个渐变或填不满的情况。以下图所示:

图中左侧是精确设置渐变出发点和终点为矩形的顶部和底部; 图中中间为设置的渐变出发点为顶部,终点为矩形的中间; 右侧的则设置的渐变出发点和终点都大于矩形的顶部和底部。代码以下:

LinearGradient gradient = new LinearGradient(100, mHeight_2 - 200, 100, mHeight_2 + 200,
        line_1_start_color, region_1_end_color, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(100, mHeight_2 - 200, 300, mHeight_2 + 200, mPaint);

gradient = new LinearGradient(400, mHeight_2 - 200, 400, mHeight_2,
         line_1_start_color, region_1_end_color, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(400, mHeight_2 - 200, 600, mHeight_2 + 200, mPaint);

gradient = new LinearGradient(700, mHeight_2 - 400, 700, mHeight_2 + 400,
        line_1_start_color, region_1_end_color, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(700, mHeight_2 - 200, 900, mHeight_2 + 200, mPaint);

对矩形这类规则图形进行渐变填充,能够很容易设置渐变色彩的出发点和终点。 但是对上图中的正弦曲线如果做到呢? 难道需要将1组正弦曲线的每一个点上下连接,使用渐变进行绘制? 那样计算量将会是非常巨大的!那又有其他甚么好的方法呢?

Paint 中提供了 Xfermode 图象混合模式的机制。 它能够控制绘制图形与之前已存在图形的混合交叠模式。其中比较有用的是 PorterDuffXfermode 这个类。它有多种混合模式,以下图所示:

这里 canvas 原本的图片可以理解为背景,就是 dst; 新画上去的图片可以理解为前景,就是 src。有了这类图形混合技术,能够完成各种图形交集的显示。

那我们是不是可以脑洞大开1下,将上图已绘制好的波形图,与渐变的矩形进行交集,将它们相交的地方画出来呢。 它们相交的地方好像恰好就是我们需要的效果呢。

这样,我们只需要先填充波形,然后在每组正弦线相交的封闭区域画1个以波峰和波谷为高的矩形,然后将这个矩形染色成渐变色。以这个矩形与波形做出交集,选择 SrcIn 模式,即能只显示相交部份矩形的这1块的色彩。 这个方案看起来可行,先试试。下面图是没有履行 Xfermode 的叠加图, 从图中可以看出,两个正弦线中间的区域正是我们需要的!

下面是履行 SrcIn 模式混合以后的图象:

奇异的事情出现了, 视觉图中的效果被还原了。

我们再依葫芦画瓢,再绘制另外1组正弦曲线。 这里需要注意的是,由于 Xfermode 中的 Dst 指的原本的背景,因此这里两组正弦线的混合会相互产生影响。 即第2组在调用 SrcIn 模式进行混合的时候,会将第1组的图形进行剪切。以下图所示:

因此在绘制的时候,必须将两组正弦曲线分开单独绘制在不同 Canvas 层上。 好在 Android 系统为我们提供了这个功能,Android 提供了不同 Canvas 层,以用于进行离屏缓存的绘制。我们可以先绘制1组图形,然后调用 canvas.saveLayer 方法将它存在离屏缓存中,然后再绘制另外1组曲线。最后调用 canvas.restoreToCount(sc);方法恢复 Canvas,将两屏混合显示。最后的效果图以下所示:

这里总结1下绘制的顺序:
1、计算出曲线需要绘制的点
2、填充出正弦线
3、在每组正弦线相交的地方,根据波峰波谷绘制出1个渐变线填充的矩形。并且设置图形混合模式为 SrcIn

 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 

4、对正弦线进行描边
5、离屏存储 Canvas,再进行下1组曲线的绘制

静态的绘制已完成了。接下来就是让它动起来了。 根据上面给出来的框架,在绘制线程中会定时履行 doDraw 方法。我们只需要在 doDraw 方法中每次将波形往前移动1个距离,便可到达让波形往前移动的效果。具体对应到正弦公式中的 φ 值,每次只需要在原有值的基础上修改这个值即能改变波形在 X 轴的位置。每次履行 doDraw 都会根据下面的计算方法重新计算图形的初相值:

this.mPhase = (float) ((this.mPhase + Math.PI * mSpeed) % (2 * Math.PI));

在计算波形高度的时候,还可以乘以音量大小。即正弦公式中的 A 的值可以为 volume * 绘制的最大高度 * 425/(4+x4)。 这样波形的振幅即能与音量正相干。波形可以随着音量跳动大小。

动画的优化

虽然上面已实现了波形的动画。但是如果以为工作已结束了,那就真是太 sample,naive了。

现在手机的分辨率变的愈来愈大,1般都是1080p的分辨率。随着分辨率的增加,图形绘制所需要的计算量也愈来愈大(像素点多了)。这样致使在某些低端手机中,或某些伪高端手机(比如某星S4)中,CPU 的计算能力不足,从而致使动画的卡顿。 因此对自绘动画,可能还需要不断的进行代码和算法的优化,提高绘制的效力,尽可能减少计算量。

自绘动画优化的终究目的是减少计算量,下降 CPU 的负担。为了到达这个目的,笔者总结归纳了以下几种方法,如果大家有更多更好的方法,欢迎分享:

1、下降分辨率

在实际动画绘制的进程中,如果对每一个像素点的去计算(x,y)值,会致使大量的计算。但是这类密集的计算常常都是不需要的。 对动画,人的肉眼是有1定的容忍度的,在1定范围内的图形失真是没法发觉的,特别是那种1闪而过的东西更是如此。 这样在实现的时候,可以都自己拟定1个比实际分辨率小很多的图形密度,这个图形密度上来计算 Y 值。然后将我们自己定义的图形密度成比例的映照到真实的分辨率上。 比如上面绘制正弦曲线的时候,我们完全可以只计算100个点。然后将这60个点成比例的放在1024个点的X轴上。 这样我们1下子便减少了接近10倍的计算量。这有点类似栅格化1副图片。

由于采取了低密度的绘制,将这些低密度的点用直线连接起来,会产生锯齿的现象,这样一样会对体验产生影响。但是别怕,Android 已为我们提供了抗锯齿的功能。在 Paint 类中便可进行设置:

mPaint.setAntiAlias(true);

使用 Android 优化过了的抗锯齿功能,1定会比我们每一个点的去绘制效力更高。

通过动态调理自定义的绘制密度,在绘制密度与终究实现效果中找到1个平衡点(即不影响最后的视觉效果,同时还能最大限度的减少计算量),这个是最直接,也最简单的优化方法。

2、减少实时计算量

我们知道在过去嵌入式装备中计算资源都是相当有限的,运行的代码常常需要优化,乃至有时候需要在汇编级别进行。虽然现在手机中的处理器已愈来愈强大,但是在处理动画这类短时间间隔的大量运算,还是需要仔细的编写代码。 1般的动画刷新周期是16ms,这也意味着动画的计算需要尽量的少做运算。

只要能够减少实时计算量的事情,都应当是我们应当做的。那末如何才能做到尽可能少做实时运算呢? 1个比较重要的思惟和方法是利用用空间来换取时间。1般我们在做自绘动画的时候,会需要做大量的中间运算。而这些运算有可能在每次绘制定时到来的时候,产生的结果都是1样的。这也意味着有可能我们重复的做出了需要冗余的计算。 我们可以将这些中间运算的结果,存储在内存中。这样下次需要的时候,便不再需要重新计算,只需要取出来直接使用便可。 比较经常使用的查表法即便利用这类空间换时间的方法来提高速度的。

具体针对本例而言, 在计算 425/(4+x4) 这个衰减系数的时候,对每一个 X 轴上固定点来讲,它的计算结果都是相同的。 因此我们只需要将每一个点对应的 y 值存储在1个数组中,每次直接从这个数组中获得便可。这样能够节省出很多 CPU 在计算乘方和除法运算的计算量。 一样道理,由于 sin 函数具有周期性,因此我们只需要将这个周期中的固定 N 个点计算出值,然后存储在数组中。每次需要计算 sin 值的时候,直接从之前已计算好的结果中找出近似的那个就能够了。 固然其实这里计算 sin 不需要我们做这样的优化,由于 Android 系统提供的 Math 方法库中计算 sin 的方法肯定已应用类似的原理优化过了。

CPU 1般都有1个特点,它在快速的处理加减乘运算,但是在处理浮点型的除法的时候,则会变的特别的慢,多要多个指令周期才能完成。因此我们还应当努力减少运算量,特别是浮点型的除法运算。 1般比较通用的做法是讲浮点型的运算转换成整型的运算,这样对速度的提升也会比较明显。 但是整型运算同时也意味着会丢失数据的精确度,这样常常会致使绘制出来的图形有锯齿感。 之前有同事便遇到即便采取了 Android 系统提供的抗锯齿方法,但是绘制出来的图形锯齿感还是很强烈,有可能就是数值计算中的精确度的问题,比如采取了不正确的整型计算,或毛病的4舍5入。 为了保证精确度,同时还能使用整型来进行运算,常常可以将需要计算的参数,统1乘上1个精确度(比如乘以100或1000,视需要的精确范围而定)取整计算,最后再将结果除以这个精确度。 这里还需要注意整型溢出的问题。

3、减少内存分配次数

Android 在内存分配和释放方面,采取了 JAVA 的垃圾回收 GC 模式。 当分配的内存不再使用的时候,系统会定时帮我们自动清算。这给我们利用开发带来了极大的便利,我们从此不再需要过量的关注内存的分配与回收,也因此减少很多内存使用的风险。但是内存的自动回收,也意味着会消耗系统额外的资源。1般的 GC 进程会消耗系统ms级别的计算时间。在普通的场景中,开发者无需过量的关心内存的细节。但是在自绘动画开发中,却不能疏忽内存的分配。

由于动画1般由1个16ms的定时器来进行驱动,这意味着动画的逻辑代码会在短时间内被循环往复的调用。 这样如果在逻辑代码中在堆上创建过量的临时变量,会致使内存的使用量在短时间稳步上升,从而频繁的引发系统的GC行动。这样无疑会拖累动画的效力,让动画变得卡顿。

处理分析内存分配,减少没必要要的分配呢, 首先我们需要先分析内存的分配行动。 对Android内存的使用情况,Android Studio提供了很好用,直观的分析工具。 为了更加直观的表现内存分配的影响,在程序中故意创建了1些比较大的临时变量。然后使用Memory Monitor工具得到了下面的图:

并且在log中看到有频繁的打印D/dalvikvm: GC_FOR_ALLOC freed 3777K, 18% free 30426K/36952K, paused 33ms, total 34ms

图中每次涨跌的锯齿意味着产生了1次GC,然后又分配了多个内存,这个进程不断的往复。 从log中可以看到系统在频繁的发起GC,并且每次GC都会将系统暂停33ms,这固然会对动画造成影响。 固然这个是测试的比较极真个情况,1般来讲,如果内存被更加稳定的使用的话,触发GC的几率也会大大的下降,上面图中的颠簸锯齿出现到几率也会越低。

上面内存使用的情况,也被称为内存抖动,它除在周期性的调用进程中出现,另外1个多发场景是在for循环中分配、释放内存。它影响的不单单是自绘动画中,其他场景下也需要尽可能避免。

从上图中可以直观的看到内存在1定时间段内分配和释放的情况,得出是不是内存的使用是不是安稳。但是当出现问题以后,我们还需要借助 Allocation Tracker 这个工具来追踪问题产生的缘由,并最后解决它。Allocation Tracker 这个工具能够帮助我们追踪内存对象的分配和释放情况,能够获得内存对象的来源。比如上面的例子,我们在1段时间内进行追踪,可以得到以下图:

从图中我们可以看到大部份的内存分配都来自线程18 Thread 18,这也是我们的动画的绘制线程。 从图中可以看到主要的内存分配有以下几个地方:
1、我们故意创建的临时大数组
2、来自 getColor 函数, 它来自对 getResources().getColor()的调用,需要获得从系统资源中获得色彩资源。这个方法中会创建多个 StringBuilder 的变量
3、创建 Xfermode 的临时变量,来自 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 这个调用。

4、创建渐变值的 LinearGradient gradient = new LinearGradient(getXPos(startX), startY, getXPos(startX), endY,
gradientStartColor, gradientEndColor, Shader.TileMode.REPEAT);

对第2、3,这些变量完全不需要每次循环履行的时候,重复创建变量。 由于每次他们的使用都是固定的。可以斟酌将它们从临时变量转为成员变量,在动画初始化的同时也将这些成员变量初始化好。需要的时候直接调用便可。

而对第4类这样的内存分配,由于每次动画中的波形形状都不1样,因此渐变色必现得重新创建并设值。因此这里其实不能将它作为成员变量使用。这里是属于必须要分配的。好在这个对象也不大,影响很小。

对那些没法避免,每次又必须分配的大量对象,我们还能够采取对象池模型的方式来分配对象。对象池来解决频繁创建与烧毁的问题,但是这里需要注意结束使用以后,需要手动释放对象池中的对象。

经过优化的内存分配,会变得平缓很多。比如对上面的例子。 去除上面故意创建的大量数组,和优化了2、3两个点以后的内存分配以下图所示:

可以看出短时间内,内存并没有甚么明显的变化。并且在很长1段时间内都没有触发1次 GC

4、减少 Path 的创建次数

这里触及到对特殊规则图形的绘制的优化。 Path 的创建也触及到内存的分配和释放,这些都是需要消耗资源的。并且对越复杂的 Path,Canvas 在绘制的时候,也会更加的耗时。因此我们需要做的就是尽可能优化 Path 的创建进程,简化运算量。这1块并没有很多统1的标准方法,更多的是依托经验,并且将上面提到到的3点优化方法灵活应用。

首先 Path 类中本身即提供了数据结构重用的接口。它除提供 reset 复位方法以外,还提供了 rewind 的方法。这样每次动画循环调用的时候,能够做到不释放之前已分配的内存就可以够重用。这样避免的内存的反复释放和分配。特别是对本例中,每次绘制的 Path 中的点都是1样多的情况更加适用。

采取方法1种低密度的绘图方法,一样还能够减少 Path 中线段的数量,这样下降了 Path 构造的次数,同能 Canvas 在绘制 Path 的时候,由于 Path 变的简单了,一样能够加快绘制速度。

特别的,对本文中的波形例子。 视觉图中给出来的效果图,除要用渐变色填充正弦线中间的区域以外。还需要对正弦线本身进行描边。 同时1组正弦线中的上下两根正弦线的色彩还不1样。 这样对1组完全的正弦线的绘制其实需要3个步骤:
1、填充正弦线
2、描正弦线上边沿
3、描正弦线下边沿

如何很好的将这3个步骤组合起来,尽可能减少 Path 的创建也很有讲求。比如,如果我们直接依照上面列出来的步骤来绘制的话,首先需要创建1个同时包括上下正弦线的 Path,需要计算1遍上下正弦线的点,然后对这个 Path 使用填充的方式来绘制。 然后再计算1遍上弦线的点,创建只有上弦线的 Path,然后使用 Stroke 的模式来绘制,接着下弦线。 这样我们将会重复创建两边 Path,并且还会重复1倍点坐标的计算量。

如果我们能采取上面步骤2中提到的,利用空间换取时间的方法。 首先把所有点位置都记在1个数组中,然后利用这些点来计算并绘制上弦线的 Path,然后保存下来;再计算和绘制下弦线的 Path 并保存。最后创建1个专门记录填充区的 Path,利用 mPath.addPath();的功能,将之前的两个 path 填充到该 Path 中。 这样便能够减少 Path 的计算量。同时将3个 Path 分别用不同的变量来记录,这样在下次循环到来的时候,还能利用 rewind 方法来进行内存重用。

这里需要注意的是,Path 提供了 close的方法,来将1段线封闭。 这个函数能够提供1定的方便。但是其实不是每一个时候都好用。有的时候,还是需要我们手动的去添加线段来闭合1个区域。比以下面图中的情形,采取 close,就会致使中间有1段空白的区域:

5、优化绘制的步骤

甚么? 经过上面几个步骤的优化,动画还是卡顿?不要慌,这里再提供1个精确分析卡顿的工具。 Android 还为我们提供了能够追踪监控每一个方法履行时间的工具 TraceView。 它在 Android Device Monitor 中打开。比如笔者在开发进程中发现动画有卡顿,然后用上面 TraceView 工具查看得到下图:

发现 clapGradientRectAndDrawStroke 这个方法占用了72.1%的 CPU 时间,而这个方法中实际占用时间的是 drawPath。这说明此处的绘制存在明显的缺点与不公道,大部份的时间都用在绘制 clapGradientRectAndDrawStroke 上面了。那末我们再看1下之前绘制的原理,为了能够从矩形和正弦线之间剪切出交集,并显示渐变区域。笔者做出了以下图的尝试:

首先绘制出渐变填充的矩形; 然后再将正弦线包裹的区域用透明色彩进行反向填充(白色区域),这样它们交集的地方利用 SrcIn 模式进行剪切,这时候候显示出来便是白色覆盖了矩形的区域(实际是透明色)加上它们未交集的地方(正弦框内)。这样一样能够到达设计图中给出的效果。代码以下:

    mPath.rewind();
    mPath.addPath(mPathLine1);
    mPath.lineTo(getXPos(mDensity - 1), -mLineCacheY[mDensity - 1] + mHeight_2 * 2);
    mPath.addPath(mPathLine2);
    mPath.lineTo(getXPos(0), mLineCacheY[0]);

    mPath.setFillType(Path.FillType.INVERSE_WINDING);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setShader(null);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    mPaint.setColor(getResources().getColor(android.R.color.transparent));
    canvas.drawPath(mPath, mPaint);
    mPaint.setXfermode(null);

虽然上面的代码一样也实现了效果,但是由于使用的反向填充,致使填充区域急剧变大。最后致使 canvas.drawPath(mPath, mPaint);调用占据了70%以上的计算量。

找到瓶颈点并知道缘由以后,我们就可以做出针对性的改进。 我们只需要调剂绘制的顺序,先将正弦线区域内做正向填充,然后再以 SrcIn 模式绘制渐变色填充的矩形。 这样减少了需要绘制的区域,同时也到达预期的效果。

下面是改进以后 TraceView 的结果截图:

从截图中可以看到计算量被均分到不同的绘制方法中,已没有瓶颈点了,并且实测动画也变得流畅了。 1般卡顿都能通过此种方法比较精确的找到真实的瓶颈点。

总结

本文主要简单介绍了1下 Android 普通 View 和 SurfaceView 的绘制与动画原理,然后介绍了1下录音机波形动画的具体实现和优化的方法。但是限于笔者的水平和经验有限,肯定有很多纰漏和毛病的地方。大家有更多更好的建议,欢迎1起分享讨论,共同进步。

更多精彩内容欢迎关注腾讯bugly微信公众账号:

腾讯bugly

腾讯bugly

腾讯 Bugly是1款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上利用崩溃的情况和解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,逐日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解利用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐