標(biāo)簽: 自定義view 音量波形 音波
本文目的:主要是記錄自己在實(shí)現(xiàn)自定義view的時(shí)候,一些思路和解決方案。
目標(biāo)

繪制兩個(gè)音量波形,并且能夠向右運(yùn)動(dòng),上面的波形移動(dòng)速度慢,下面的波形移動(dòng)速度快,并且振幅能夠根據(jù)音量的高低進(jìn)行改變。
分解目標(biāo)
先考慮靜止?fàn)顟B(tài),上圖有兩個(gè)波形圖,現(xiàn)只考慮一個(gè)波形圖,每個(gè)波形圖類似于兩個(gè)正弦函數(shù)的閉合。所以我們第一步要繪制一個(gè)正弦圖形。

繪制正弦函數(shù)
關(guān)于自定義view的圖形繪制,一般都需要onMeasure,onLayout,onDraw三個(gè)步驟。由于是自定義view,而不是viewGroup,所以并不需要實(shí)現(xiàn)onLayout方法。
在繪制之前,要在onMeasure方法里,計(jì)算出畫布的高度、寬度、中心點(diǎn)等需要計(jì)算的變量,這里就不詳細(xì)說明了。
為了便于繪制圖形正弦函數(shù),要把畫布的坐標(biāo)原點(diǎn)移動(dòng)到繪制view的中間位置。
也就是下圖中標(biāo)明的點(diǎn),這樣坐標(biāo)原點(diǎn)(0,0),就位于view的中間,便于函數(shù)計(jì)算。
正弦函數(shù)方法參考:
private double sine(float x, int period, float drawWidth) {
return Math.sin(2 * Math.PI * period * x / drawWidth);
}
其中period為在畫布里有多少個(gè)周期,假設(shè)period為3,就是在畫布里有三個(gè)周期。drawWidth為畫布寬度。
在ondraw 方法里進(jìn)行繪制。
這里調(diào)用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);//將繪制的起點(diǎn)移動(dòng)到最左邊
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 為振幅的高度,也就是半個(gè)畫布的高度。繪制出的圖形如下(在手機(jī)里,y軸正方形是向下的,x軸正方形是向右的)

繪制兩個(gè)關(guān)于y軸對(duì)稱正弦函數(shù)
繪制反方向正弦函數(shù),并且填充里面的內(nèi)容。只是相當(dāng)于將y值乘以-1,這里不詳細(xì)列出具體代碼

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

這樣勉強(qiáng)能算作一個(gè)波形圖了。
縮放波形圖
觀察剛開始的效果圖,發(fā)現(xiàn)每個(gè)波形的振幅并不相同,所以要考慮對(duì)波形圖進(jìn)行縮放。
采用縮放函數(shù),就是按比例將振幅逐漸增大或者減小。
double scaling;
for (float x = -halfDrawWidth; x <= halfDrawWidth; x++) {
scaling = 1 - Math.pow(x / halfDrawWidth, 2);// 對(duì)y進(jìn)行縮放
y = (float) (sine(x, period, drawWidth) * amplitude * (1) * Math
.pow(scaling, 3));
path.lineTo(x, y);
}
為了更好的效果,我們縮放了三次,Math.pow(scaling, 3)
現(xiàn)在感覺和圖一的效果差不多了。基本滿足需求,就是每個(gè)波形之間的間隙還是很小。(后續(xù)會(huì)進(jìn)行優(yōu)化)

讓波形圖動(dòng)起來
在view里定義一個(gè)移動(dòng)線程MoveThread,每隔一段時(shí)間就執(zhí)行一次刷新postInvalidate(),每次刷新圖像的時(shí)候,都會(huì)改變?cè)搱D形的相位。
所謂相位,查看下圖,一個(gè)函數(shù)是sin(x),另外一個(gè)函數(shù)是sin(x+0.5),兩個(gè)函數(shù)之間就相差了0.5個(gè)相位。

相位變化了0.5,看起來就會(huì)向左移動(dòng)0.5的距離。(圖形右上角有標(biāo)注函數(shù))
在線程中不斷更新相位的取值,這樣不斷的刷新圖形,就會(huì)看起來形成一種移動(dòng)的效果。(大家可以想象以前放電影時(shí)用的膠片,實(shí)現(xiàn)原理類似)這樣我們的圖形就能運(yùn)動(dòng)起來了。
修改后的sine函數(shù)
private double sine(float x, int period, float drawWidth, double phase) {
return Math.sin(2 * Math.PI * period * (x + phase) / drawWidth);
}
定義一個(gè)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;
}
}
這樣當(dāng)線程開啟的時(shí)候,我們就能根據(jù)不斷的改變sine函數(shù)的相位,就會(huì)形成不斷右移動(dòng)的效果。
繪制兩個(gè)波形,并且設(shè)置不同的移動(dòng)速度
兩個(gè)波形的區(qū)別只是顏色不同,最大振幅不同,以及移動(dòng)速度不同。
所謂移動(dòng)速度不同,就是相位每次改變的值不同??梢栽谟?jì)算sine函數(shù)的時(shí)候,對(duì)固定相位值乘以不同的比例,就會(huì)得到不同的移動(dòng)速度。從下圖中的移動(dòng)我們可以看到效果,已經(jīng)很接近目標(biāo)了。

(這里可以在圖中看到不同的實(shí)現(xiàn)效果,為了便于有些同學(xué)學(xué)習(xí)和實(shí)踐,將整個(gè)view進(jìn)行了解剖,能更快的學(xué)習(xí)view的繪制過程)
根據(jù)音量改變波形圖的振幅
通過音量設(shè)置波形圖振幅,這樣能夠讓波形圖隨著聲音大小的變化而變化。
我們改變sin函數(shù)的振幅,圖形就會(huì)升高或者下降。也就是在相同的x位置處,y的取值會(huì)發(fā)生變化。

但是,隨著音頻的變化,振幅的變動(dòng)幅度變大,這樣會(huì)造成一種圖形的閃動(dòng)。
解決圖形閃動(dòng)
當(dāng)音量變化時(shí),我們的振幅會(huì)發(fā)生變化,也就是這個(gè)圖形,會(huì)隨著振幅的變化按比例變大或者變小。如下圖標(biāo)記的兩個(gè)點(diǎn),如果我們刷新間隔為1s,就是1s之后,點(diǎn)1會(huì)突然變成點(diǎn)2的位置。這樣就會(huì)造成閃動(dòng)。

我們的要求是圖形要平滑的變動(dòng),意思就是不能這么快的進(jìn)行變化,要怎么解決呢?
首先我們規(guī)定上升的最大速度為為1px每秒,現(xiàn)在的y值為1px,也就是當(dāng)前1的位置。
現(xiàn)在只考慮點(diǎn)1的位置,假設(shè)我們每1s刷新一次,上升的最大速度為1px每秒,這樣我們就可以計(jì)算出下一次變化y的最高位置為 1px + 1px/秒 * 1秒 = 2。
- 如果當(dāng)前音量發(fā)生變化,也就是振幅發(fā)生改變,得到的y值為3px,這個(gè)時(shí)候y值,3px >
我們計(jì)算的2px,這個(gè)時(shí)候就要用我們的2px。也就保證了最大速度不能超過我們規(guī)定的速度。 - 如果當(dāng)前音量發(fā)生變化,也就是振幅發(fā)生改變,得到的y值為1.5px,這個(gè)時(shí)候y值,1.5px <
我們計(jì)算的2px,這個(gè)時(shí)候就要用我們的1.5px。根據(jù)實(shí)際位置進(jìn)行設(shè)定。
下降同理,這樣我們就能保證上升或者下降的最大速度。
// 計(jì)算當(dāng)前時(shí)間下的振幅
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;
}
圖形優(yōu)化
因?yàn)橹虚g的間隙過小,我們要把中間的間歇變大,類似于下圖。這樣效果可能會(huì)更好一點(diǎn)。

實(shí)施方案,將正弦函數(shù)上移,下面的正弦函數(shù)下移動(dòng),這樣中間留有固定寬度的,通過縮放函數(shù)之后,效果如下:

實(shí)驗(yàn)過程中存在的問題以及解決方案:
中間線條的問題
橫線的原因,是因?yàn)榭s放造成了這 兩個(gè)波形之間的點(diǎn) x對(duì)應(yīng)的值,y不等于0,會(huì)閉合不到中間的點(diǎn)。造成這個(gè)的現(xiàn)象是因?yàn)槲覀冎皇轻槍?duì)半個(gè)正弦曲線就進(jìn)行填充了

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

閃動(dòng)問題
參考上文解決方案
源代碼地址:https://github.com/duchao/VolumeView
可以直接使用的view
VolumeView.java
API: start() 開始
stop() 結(jié)束
setVolume(float volume) 設(shè)置音量