標簽: 自定義view 音量波形 音波
本文目的:主要是記錄自己在實現(xiàn)自定義view的時候颜骤,一些思路和解決方案唧喉。
目標
繪制兩個音量波形,并且能夠向右運動忍抽,上面的波形移動速度慢八孝,下面的波形移動速度快,并且振幅能夠根據音量的高低進行改變鸠项。
分解目標
先考慮靜止狀態(tài)干跛,上圖有兩個波形圖,現(xiàn)只考慮一個波形圖祟绊,每個波形圖類似于兩個正弦函數(shù)的閉合楼入。所以我們第一步要繪制一個正弦圖形。
繪制正弦函數(shù)
關于自定義view的圖形繪制久免,一般都需要onMeasure浅辙,onLayout,onDraw三個步驟阎姥。由于是自定義view记舆,而不是viewGroup,所以并不需要實現(xiàn)onLayout方法呼巴。
在繪制之前泽腮,要在onMeasure方法里御蒲,計算出畫布的高度、寬度诊赊、中心點等需要計算的變量厚满,這里就不詳細說明了。
為了便于繪制圖形正弦函數(shù)碧磅,要把畫布的坐標原點移動到繪制view的中間位置碘箍。
也就是下圖中標明的點,這樣坐標原點(0,0)鲸郊,就位于view的中間丰榴,便于函數(shù)計算。
正弦函數(shù)方法參考:
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軸對稱正弦函數(shù)
繪制反方向正弦函數(shù)惭每,并且填充里面的內容骨饿。只是相當于將y值乘以-1亏栈,這里不詳細列出具體代碼
進行內容填充
mPaint.setStyle(Style.FILL); 畫筆的樣式設置為填充台腥,填充后的效果如下
這樣勉強能算作一個波形圖了。
縮放波形圖
觀察剛開始的效果圖绒北,發(fā)現(xiàn)每個波形的振幅并不相同黎侈,所以要考慮對波形圖進行縮放。
采用縮放函數(shù)闷游,就是按比例將振幅逐漸增大或者減小峻汉。
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)
現(xiàn)在感覺和圖一的效果差不多了休吠。基本滿足需求业簿,就是每個波形之間的間隙還是很小瘤礁。(后續(xù)會進行優(yōu)化)
讓波形圖動起來
在view里定義一個移動線程MoveThread,每隔一段時間就執(zhí)行一次刷新postInvalidate()梅尤,每次刷新圖像的時候柜思,都會改變該圖形的相位岩调。
所謂相位,查看下圖赡盘,一個函數(shù)是sin(x),另外一個函數(shù)是sin(x+0.5)号枕,兩個函數(shù)之間就相差了0.5個相位。
相位變化了0.5陨享,看起來就會向左移動0.5的距離葱淳。(圖形右上角有標注函數(shù))
在線程中不斷更新相位的取值,這樣不斷的刷新圖形抛姑,就會看起來形成一種移動的效果蛙紫。(大家可以想象以前放電影時用的膠片,實現(xiàn)原理類似)這樣我們的圖形就能運動起來了途戒。
修改后的sine函數(shù)
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函數(shù)的相位,就會形成不斷右移動的效果喷斋。
繪制兩個波形唁毒,并且設置不同的移動速度
兩個波形的區(qū)別只是顏色不同,最大振幅不同星爪,以及移動速度不同浆西。
所謂移動速度不同,就是相位每次改變的值不同顽腾〗悖可以在計算sine函數(shù)的時候,對固定相位值乘以不同的比例抄肖,就會得到不同的移動速度久信。從下圖中的移動我們可以看到效果,已經很接近目標了漓摩。
(這里可以在圖中看到不同的實現(xiàn)效果裙士,為了便于有些同學學習和實踐,將整個view進行了解剖管毙,能更快的學習view的繪制過程)
根據音量改變波形圖的振幅
通過音量設置波形圖振幅腿椎,這樣能夠讓波形圖隨著聲音大小的變化而變化。
我們改變sin函數(shù)的振幅夭咬,圖形就會升高或者下降啃炸。也就是在相同的x位置處,y的取值會發(fā)生變化卓舵。
但是,隨著音頻的變化,振幅的變動幅度變大训枢,這樣會造成一種圖形的閃動托修。
解決圖形閃動
當音量變化時,我們的振幅會發(fā)生變化恒界,也就是這個圖形睦刃,會隨著振幅的變化按比例變大或者變小。如下圖標記的兩個點十酣,如果我們刷新間隔為1s涩拙,就是1s之后,點1會突然變成點2的位置耸采。這樣就會造成閃動兴泥。
我們的要求是圖形要平滑的變動虾宇,意思就是不能這么快的進行變化搓彻,要怎么解決呢?
首先我們規(guī)定上升的最大速度為為1px每秒嘱朽,現(xiàn)在的y值為1px旭贬,也就是當前1的位置。
現(xiàn)在只考慮點1的位置搪泳,假設我們每1s刷新一次稀轨,上升的最大速度為1px每秒,這樣我們就可以計算出下一次變化y的最高位置為 1px + 1px/秒 * 1秒 = 2岸军。
- 如果當前音量發(fā)生變化奋刽,也就是振幅發(fā)生改變,得到的y值為3px艰赞,這個時候y值佣谐,3px >
我們計算的2px,這個時候就要用我們的2px猖毫。也就保證了最大速度不能超過我們規(guī)定的速度台谍。 - 如果當前音量發(fā)生變化,也就是振幅發(fā)生改變吁断,得到的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;
}
圖形優(yōu)化
因為中間的間隙過小,我們要把中間的間歇變大沛厨,類似于下圖捌肴。這樣效果可能會更好一點埃跷。
實施方案,將正弦函數(shù)上移,下面的正弦函數(shù)下移動瓣铣,這樣中間留有固定寬度的,通過縮放函數(shù)之后尼酿,效果如下:
實驗過程中存在的問題以及解決方案:
中間線條的問題
橫線的原因恭理,是因為縮放造成了這 兩個波形之間的點 x對應的值,y不等于0剿牺,會閉合不到中間的點企垦。造成這個的現(xiàn)象是因為我們只是針對半個正弦曲線就進行填充了
所以我們要將正反兩個曲線畫出來之后,把路徑閉合之后再進行填充晒来。這樣就不會出現(xiàn)上面中間有橫線的瑕疵
閃動問題
參考上文解決方案
源代碼地址:https://github.com/duchao/VolumeView
可以直接使用的view
VolumeView.java
API: start() 開始
stop() 結束
setVolume(float volume) 設置音量