(自定義view實現)音量波形圖

標簽: 自定義view 音量波形 音波


本文目的:主要是記錄自己在實現自定義view的時候,一些思路和解決方案。

目標

音量波形圖

繪制兩個音量波形,并且能夠向右運動,上面的波形移動速度慢,下面的波形移動速度快,并且振幅能夠根據音量的高低進行改變。

分解目標

先考慮靜止狀態,上圖有兩個波形圖,現只考慮一個波形圖,每個波形圖類似于兩個正弦函數的閉合。所以我們第一步要繪制一個正弦圖形

正弦函數圖像

繪制正弦函數

關于自定義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軸正方形是向右的)


正弦函數圖

繪制兩個關于y軸對稱正弦函數

繪制反方向正弦函數,并且填充里面的內容。只是相當于將y值乘以-1,這里不詳細列出具體代碼


兩個正弦函數圖

進行內容填充
mPaint.setStyle(Style.FILL); 畫筆的樣式設置為填充,填充后的效果如下


音波圖

這樣勉強能算作一個波形圖了。

縮放波形圖

觀察剛開始的效果圖,發現每個波形的振幅并不相同,所以要考慮對波形圖進行縮放。
采用縮放函數,就是按比例將振幅逐漸增大或者減小。

    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里定義一個移動線程MoveThread,每隔一段時間就執行一次刷新postInvalidate(),每次刷新圖像的時候,都會改變該圖形的相位。
所謂相位,查看下圖,一個函數是sin(x),另外一個函數是sin(x+0.5),兩個函數之間就相差了0.5個相位。


正弦函數相位 + 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進行了解剖,能更快的學習view的繪制過程)

根據音量改變波形圖的振幅

通過音量設置波形圖振幅,這樣能夠讓波形圖隨著聲音大小的變化而變化。
我們改變sin函數的振幅,圖形就會升高或者下降。也就是在相同的x位置處,y的取值會發生變化。


振幅不同,兩個正弦的高度也不同

但是,隨著音頻的變化,振幅的變動幅度變大,這樣會造成一種圖形的閃動。

解決圖形閃動

當音量變化時,我們的振幅會發生變化,也就是這個圖形,會隨著振幅的變化按比例變大或者變小。如下圖標記的兩個點,如果我們刷新間隔為1s,就是1s之后,點1會突然變成點2的位置。這樣就會造成閃動。


點在不同的振幅,所在的高度不同

我們的要求是圖形要平滑的變動,意思就是不能這么快的進行變化,要怎么解決呢?
首先我們規定上升的最大速度為為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,會閉合不到中間的點。造成這個的現象是因為我們只是針對半個正弦曲線就進行填充了


有瑕疵的波形圖

所以我們要將正反兩個曲線畫出來之后,把路徑閉合之后再進行填充。這樣就不會出現上面中間有橫線的瑕疵


修復后的波形圖

閃動問題

參考上文解決方案

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

可以直接使用的view

VolumeView.java
API: start() 開始
stop() 結束
setVolume(float volume) 設置音量

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容