標簽: 自定義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的距離。(圖形右上角有標注函數)
在線程中不斷更新相位的取值,這樣不斷的刷新圖形,就會看起來形成一種移動的效果。(大家可以想象以前放電影時用的膠片,實現原理類似)這樣我們的圖形就能運動起來了。
修改后的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) 設置音量