(自定义view实现)音量波形图

标签: 自定义view 音量波形 音波


本文目的:主要是记录自己在实现自定义view的时候,一些思路和解决方案。

目标

音量波形图

绘制两个音量波形,并且能够向右运动,上面的波形移动速度慢,下面的波形移动速度快,并且振幅能够根据音量的高低进行改变。

分解目标

先考虑静止状态,上图有两个波形图,现只考虑一个波形图,每个波形图类似于两个正弦函数的闭合。所以我们第一步要绘制一个正弦图形

(自定义view实现)音量波形图_第1张图片
正弦函数图像

绘制正弦函数

关于自定义view的图形绘制,一般都需要onMeasure,onLayout,onDraw三个步骤。由于是自定义view,而不是viewGroup,所以并不需要实现onLayout方法。
在绘制之前,要在onMeasure方法里,计算出画布的高度、宽度、中心点等需要计算的变量,这里就不详细说明了。
为了便于绘制图形正弦函数,要把画布的坐标原点移动到绘制view的中间位置。
也就是下图中标明的点,这样坐标原点(0,0),就位于view的中间,便于函数计算。

正弦函数方法参考:

    private double sine(float x, int period, float drawWidth) {
        return Math.sin(2 * Math.PI * period * x / drawWidth);
    }

其中period为在画布里有多少个周期,假设period为3,就是在画布里有三个周期。drawWidth为画布宽度。

在ondraw 方法里进行绘制。
这里调用drawsine方法

private void drawSine(Canvas canvas, Path path, Paint paint, int period, float drawWidth, float amplitude) {
    float halfDrawWidth = drawWidth / 2f;
    path.reset();
    path.moveTo(-halfDrawWidth, 0);//将绘制的起点移动到最左边
    float y;
    for (float x = -halfDrawWidth; x <= halfDrawWidth; x++) {
        y = (float) sine(x, period, drawWidth) * amplitude; 
        path.lineTo(x, y);
    }   
    canvas.drawPath(path, paint);
    canvas.save();
    canvas.restore(); 
}

amplitude 为振幅的高度,也就是半个画布的高度。绘制出的图形如下(在手机里,y轴正方形是向下的,x轴正方形是向右的)


(自定义view实现)音量波形图_第2张图片
正弦函数图

绘制两个关于y轴对称正弦函数

绘制反方向正弦函数,并且填充里面的内容。只是相当于将y值乘以-1,这里不详细列出具体代码


(自定义view实现)音量波形图_第3张图片
两个正弦函数图

进行内容填充
mPaint.setStyle(Style.FILL); 画笔的样式设置为填充,填充后的效果如下


(自定义view实现)音量波形图_第4张图片
音波图

这样勉强能算作一个波形图了。

缩放波形图

观察刚开始的效果图,发现每个波形的振幅并不相同,所以要考虑对波形图进行缩放。
采用缩放函数,就是按比例将振幅逐渐增大或者减小。

    double scaling;
    for (float x = -halfDrawWidth; x <= halfDrawWidth; x++) {
        scaling = 1 - Math.pow(x / halfDrawWidth, 2);// 对y进行缩放
        y = (float) (sine(x, period, drawWidth) * amplitude * (1) * Math
                .pow(scaling, 3));
        path.lineTo(x, y);
    }

为了更好的效果,我们缩放了三次,Math.pow(scaling, 3)
现在感觉和图一的效果差不多了。基本满足需求,就是每个波形之间的间隙还是很小。(后续会进行优化)


(自定义view实现)音量波形图_第5张图片
音波缩放图

让波形图动起来

在view里定义一个移动线程MoveThread,每隔一段时间就执行一次刷新postInvalidate(),每次刷新图像的时候,都会改变该图形的相位。
所谓相位,查看下图,一个函数是sin(x),另外一个函数是sin(x+0.5),两个函数之间就相差了0.5个相位。


(自定义view实现)音量波形图_第6张图片
正弦函数相位 + 0.5 图

相位变化了0.5,看起来就会向左移动0.5的距离。(图形右上角有标注函数)
在线程中不断更新相位的取值,这样不断的刷新图形,就会看起来形成一种移动的效果。(大家可以想象以前放电影时用的胶片,实现原理类似)这样我们的图形就能运动起来了。
修改后的sine函数

private double sine(float x, int period, float drawWidth, double phase) {
    return Math.sin(2 * Math.PI * period * (x + phase) / drawWidth);
}

定义一个MoveThread

private class MoveThread extends Thread {
    private static final int MOVE_STOP = 1;

    private static final int MOVE_START = 0;

    private int state;

    @Override
    public void run() {
        mPhase = 0;
        state = MOVE_START;
        while (true) {
            if (state == MOVE_STOP) {
                break;
            }
            try {
                sleep(30);
            } catch (InterruptedException e) {
            }
            mPhase -= MOVE_DISTACE;
            postInvalidate();
        }
    }
    public void stopRunning() {
        state = MOVE_STOP;
    }
}

这样当线程开启的时候,我们就能根据不断的改变sine函数的相位,就会形成不断右移动的效果。

绘制两个波形,并且设置不同的移动速度

两个波形的区别只是颜色不同,最大振幅不同,以及移动速度不同。
所谓移动速度不同,就是相位每次改变的值不同。可以在计算sine函数的时候,对固定相位值乘以不同的比例,就会得到不同的移动速度。从下图中的移动我们可以看到效果,已经很接近目标了。


(自定义view实现)音量波形图_第7张图片
音波移动图形

(这里可以在图中看到不同的实现效果,为了便于有些同学学习和实践,将整个view进行了解剖,能更快的学习view的绘制过程)

根据音量改变波形图的振幅

通过音量设置波形图振幅,这样能够让波形图随着声音大小的变化而变化。
我们改变sin函数的振幅,图形就会升高或者下降。也就是在相同的x位置处,y的取值会发生变化。


(自定义view实现)音量波形图_第8张图片
振幅不同,两个正弦的高度也不同

但是,随着音频的变化,振幅的变动幅度变大,这样会造成一种图形的闪动。

解决图形闪动

当音量变化时,我们的振幅会发生变化,也就是这个图形,会随着振幅的变化按比例变大或者变小。如下图标记的两个点,如果我们刷新间隔为1s,就是1s之后,点1会突然变成点2的位置。这样就会造成闪动。


(自定义view实现)音量波形图_第9张图片
点在不同的振幅,所在的高度不同

我们的要求是图形要平滑的变动,意思就是不能这么快的进行变化,要怎么解决呢?
首先我们规定上升的最大速度为为1px每秒,现在的y值为1px,也就是当前1的位置。
现在只考虑点1的位置,假设我们每1s刷新一次,上升的最大速度为1px每秒,这样我们就可以计算出下一次变化y的最高位置为 1px + 1px/秒 * 1秒 = 2。

  • 如果当前音量发生变化,也就是振幅发生改变,得到的y值为3px,这个时候y值,3px >
    我们计算的2px,这个时候就要用我们的2px。也就保证了最大速度不能超过我们规定的速度。
  • 如果当前音量发生变化,也就是振幅发生改变,得到的y值为1.5px,这个时候y值,1.5px <
    我们计算的2px,这个时候就要用我们的1.5px。根据实际位置进行设定。

下降同理,这样我们就能保证上升或者下降的最大速度。

    // 计算当前时间下的振幅
    private float currentVolumeAmplitude(long curTime) {
        if (lastAmplitude == nextTargetAmplitude) {
            return nextTargetAmplitude;
        }

        if (curTime == amplitudeSetTime) {
            return lastAmplitude;
        }

        if (nextTargetAmplitude > lastAmplitude) {
            float target = lastAmplitude + mVerticalSpeed
                    * (curTime - amplitudeSetTime) / 1000;
            if (target >= nextTargetAmplitude) {
                target = nextTargetAmplitude;
                lastAmplitude = nextTargetAmplitude;
                amplitudeSetTime = curTime;
                nextTargetAmplitude = mMinAmplitude;
            }
            return target;
        }

        if (nextTargetAmplitude < lastAmplitude) {
            float target = lastAmplitude - mVerticalRestoreSpeed
                    * (curTime - amplitudeSetTime) / 1000;
            if (target <= nextTargetAmplitude) {
                target = nextTargetAmplitude;
                lastAmplitude = nextTargetAmplitude;
                amplitudeSetTime = curTime;
                nextTargetAmplitude = mMinAmplitude;
            }
            return target;
        }

        return mMinAmplitude;
    }

图形优化

因为中间的间隙过小,我们要把中间的间歇变大,类似于下图。这样效果可能会更好一点。


优化后的波形图

实施方案,将正弦函数上移,下面的正弦函数下移动,这样中间留有固定宽度的,通过缩放函数之后,效果如下:


优化后的音量波形图

实验过程中存在的问题以及解决方案:

中间线条的问题

横线的原因,是因为缩放造成了这 两个波形之间的点 x对应的值,y不等于0,会闭合不到中间的点。造成这个的现象是因为我们只是针对半个正弦曲线就进行填充了


有瑕疵的波形图

所以我们要将正反两个曲线画出来之后,把路径闭合之后再进行填充。这样就不会出现上面中间有横线的瑕疵


(自定义view实现)音量波形图_第10张图片
修复后的波形图

闪动问题

参考上文解决方案

源代码地址:https://github.com/duchao/VolumeView

可以直接使用的view

VolumeView.java
API: start() 开始
stop() 结束
setVolume(float volume) 设置音量

你可能感兴趣的:((自定义view实现)音量波形图)