話很少說,先上圖爲敬java
產品經理:「兩條曲線隨聲音大小變化而上下波動並向前滾動着」android
程序猿:「痛並快樂着」git
ok 言歸正傳,做爲一個理科生看到這條曲線首先想到的不該該是海浪、麻繩、馬尾辮...而是正弦函數 y=sin(x)github
想要它滾動起來,那麼就將圖像向右平移 y=sin(x - k)canvas
當k值隨着時間的變化而有序遞增時,圖像就會源源不斷地向右運動啦。bash
另外,要知足曲線隨聲音大小變化而上下波動的需求,只須要調節 y=A*sin(x - k) 中的A值便可:ide
至此,數學理論基礎描述完畢。函數
忍不住安利一下這個優美的函數圖像繪製工具 www.desmos.com/calculator工具
顯然,安卓原生控件沒法知足咱們這個需求,自能上自定義View的賊船了。咱們須要繼承SurfaceView去繪製波形。這裏可能有童鞋要問了,爲啥不是繼承View捏?動畫
SurfaceView和View的區別:View在主線程去更新繪製,而SurfaceView則在一個單獨的子線程中去更新繪製。View經過刷新來重繪視圖,刷新週期通常爲16毫秒,若是繪製的邏輯較複雜,則會形成丟幀或卡頓,甚至阻塞主線程。而SurfaceView是在一個線程進行繪製,並不會佔用主線程的資源。
1. 建立一個自定義的SurfaceView,繼承SurfaceView的類,並實現SurfaceHolder.Callback接口:
@Override
public void surfaceCreated(SurfaceHolder holder) {
//surfaceView的大小發生改變
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//surfaceView建立,啓動繪製線程
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//surfaceView銷燬,中止繪製線程
}
複製代碼
2. 建立繪製線程:
繪製線程爲一個死循環,按照必定的時長間隔,經過SurfaceHolder獲取canvas進行刷新和重繪操做。爲了可以實現波形暫停的功能,咱們在死循環內嵌套了一個控制繪製的開關變量。另外爲避免過分繪製,每完成一次繪製後繪製線程休眠一段時間。
private class DrawThread extends Thread {
private SurfaceHolder mHolder;
private boolean mIsRun = false;
private boolean mIsStop = false;
public DrawThread(SurfaceHolder holder) {
super(TAG);
mHolder = holder;
}
@Override
public void run() {
while (!mIsStop) {
synchronized (mSurfaceLock) {
if (mIsRun) {
Canvas canvas = null;
try {
canvas = mHolder.lockCanvas();//鎖定畫布
if (null != canvas) {
doDraw(canvas); //真正繪製
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != canvas) {
mHolder.unlockCanvasAndPost(canvas);//結束鎖定畫圖,並提交改變
}
}
}
}
try {
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setRun(boolean isRun) {
this.mIsRun = isRun;
}
public void setStop() {
this.mIsStop = true;
}
}複製代碼
3. 繪製波形線:
根據上述的公式 y = A * sin(x - k),A值-隨音量值呈正相關變化,k值-隨着時間的變化而有序遞增,x則取SurfaceView的橫向座標,這樣就能夠計算出對應的y值,再在對應的(x, y)位置繪點。而因爲屏幕的像素點不少,若是每一個點都要計算的話,對cup的消耗是很大的,所以咱們採起的改描點爲畫線的方式,隔必定的像素點計算出對應的y值,再在兩點之間畫線(想象一下看星星一顆兩顆三顆四顆連成線的樣子),這樣就能夠大大減小計算量了。
//爲下降繪製曲線時的計算量,由描點改成畫線(每20個像素畫條直線)
for (int x = 0; x < getWidth() + 20; x = x + 20) {
float topY = (float) (mWaveA * Math.sin(deltaT - WAVE_K * x));
topY = topY + mHeight / 2f;
canvas.drawLine(x - 20, lastTopY, x, topY, mPaintA);
lastTopY = topY;
}複製代碼
做爲一個有追求的自定義View,固然還要作得通用一些,例如要支持設置顏色、線條寬度、聲波最大值以及單雙線顯示等...
--------------- 我是分割線,完整代碼附上 ---------------
styles.xml
<!-- 聲波動畫-->
<declare-styleable name="SoundWaveView">
<!-- 聲波線條寬度-->
<attr name="lineWidth" format="dimension" />
<!-- 聲波線條顏色1-->
<attr name="lineTopColor" format="color" />
<!-- 聲波線條顏色2-->
<attr name="lineBottomColor" format="color" />
<!-- 最大聲波值-->
<attr name="maxVolume" format="integer" />
<!-- 聲波類型-->
<attr name="waveStyle" format="enum">
<enum name="doubleLine" value="0" />
<enum name="singleLine" value="1" />
</attr>
</declare-styleable>複製代碼
java代碼
package com.xinwei.soundwave.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.xinwei.soundwave.R;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 聲波動畫
* Created by xinwei on 2019/2/28
*/
public class SoundWaveView extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "SoundWaveView";
private static final int DEFAULT_MAX_VOLUME = 100; //默認最大聲波值
private static final int DEFAULT_LINE_WIDTH = 3; //默認線條寬度
private static final int WAVE_STYLE_DOUBLE = 0;//雙線
private static final int WAVE_STYLE_SINGLE = 1;//單線
private static final int SLEEP_TIME = 30;
private final Object mSurfaceLock = new Object();
private LinkedBlockingQueue<Integer> mVolumeQueue = new LinkedBlockingQueue<>(100);//聲波數據
private int mMaxVolume = DEFAULT_MAX_VOLUME; //最大聲波值
private boolean mIsSingleLine;//是否爲單線
private DrawThread mThread;
private Paint mPaintA, mPaintB;
private static float WAVE_K;
private static float WAVE_AMPLITUDE;
private static float WAVE_OMEGA;
private float mWaveA;
private long mBeginTime;
private int mHeight;
public SoundWaveView(Context context) {
this(context, null);
}
public SoundWaveView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SoundWaveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SoundWaveView);
int lineWidth = typedArray.getDimensionPixelSize(R.styleable.SoundWaveView_lineWidth, DEFAULT_LINE_WIDTH);
int lineTopColor = typedArray.getColor(R.styleable.SoundWaveView_lineTopColor, Color.BLUE);
int lineBottomColor = typedArray.getColor(R.styleable.SoundWaveView_lineBottomColor, Color.GREEN);
int index = typedArray.getInt(R.styleable.SoundWaveView_waveStyle, WAVE_STYLE_DOUBLE);
mMaxVolume = typedArray.getInteger(R.styleable.SoundWaveView_maxVolume, DEFAULT_MAX_VOLUME);
mIsSingleLine = (index == WAVE_STYLE_SINGLE);
typedArray.recycle();
mPaintA = new Paint();
mPaintA.setStrokeWidth(lineWidth);
mPaintA.setAntiAlias(true);
mPaintA.setColor(lineTopColor);
mPaintB = new Paint();
mPaintB.setStrokeWidth(lineWidth);
mPaintB.setAntiAlias(true);
mPaintB.setColor(lineBottomColor);
getHolder().addCallback(this);
}
/**
* 開始動畫
*/
public void start() {
Log.d(TAG, "start()");
synchronized (mSurfaceLock) { //這裏須要加鎖,不然doDraw中有可能會crash
if (null != mThread) {
mThread.setRun(true);
}
}
}
/**
* 中止動畫
*/
public void stop() {
Log.d(TAG, "stop()");
synchronized (mSurfaceLock) {
mWaveA = WAVE_AMPLITUDE;
if (null != mThread) {
mThread.setRun(false);
}
}
}
/**
* 更新數據
* @param volume 聲波值
*/
public void addData(int volume) {
mVolumeQueue.offer(volume);
}
/**
* 設置聲波最大值
* @param maxVolume 最大聲波值
*/
public void setMaxVolume(int maxVolume) {
mMaxVolume = maxVolume;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "surfaceCreated()");
mBeginTime = System.currentTimeMillis();
mThread = new DrawThread(holder);
mThread.setRun(true);
mThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
Log.d(TAG, "surfaceChanged()");
//這裏能夠獲取SurfaceView的寬高等信息
WAVE_K = 0.02f;//控制振幅
WAVE_OMEGA = 0.0025f;//控制移動速度
WAVE_AMPLITUDE = getHeight() / 2f - 5;
mHeight = height;
mWaveA = WAVE_AMPLITUDE;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "surfaceDestroyed()");
synchronized (mSurfaceLock) { //這裏須要加鎖,不然doDraw中有可能會crash
mThread.setRun(false);
mThread.setStop();
}
}
private void doDraw(Canvas canvas) {
if (null == canvas) {
return;
}
Integer volume = mVolumeQueue.poll();
//DebugLog.d(TAG, "doDraw() volume = " + volume);
if (null != volume) {
if (volume > mMaxVolume) {
volume = mMaxVolume;
}
mWaveA = (mWaveA + (float) (WAVE_AMPLITUDE * (0.2 + 0.8 * volume / mMaxVolume))) / 2f;//爲保證波形的最小振幅不爲零而做了換算
}
double deltaT = ((System.currentTimeMillis() - mBeginTime)) * WAVE_OMEGA;
canvas.drawColor(Color.WHITE);
float lastTopY = 0;
float lastButtonY = 0;
//爲下降繪製曲線時的計算量,由描點改成畫線(每20個像素畫條直線)
for (int x = 0; x < getWidth() + 20; x = x + 20) {
//畫線1
float topY = (float) (mWaveA * Math.sin(deltaT - WAVE_K * x));
topY = topY + mHeight / 2f;
canvas.drawLine(x - 20, lastTopY, x, topY, mPaintA);
lastTopY = topY;
//畫線2
if (!mIsSingleLine) {
float buttonY = mHeight - topY;
canvas.drawLine(x - 20, lastButtonY, x, buttonY, mPaintB);
lastButtonY = buttonY;
}
}
}
private class DrawThread extends Thread {
private SurfaceHolder mHolder;
private boolean mIsRun = false;
private boolean mIsStop = false;
public DrawThread(SurfaceHolder holder) {
super(TAG);
mHolder = holder;
}
@Override
public void run() {
while (!mIsStop) {
synchronized (mSurfaceLock) {
if (mIsRun) {
Canvas canvas = null;
try {
canvas = mHolder.lockCanvas();//鎖定畫布
if (null != canvas) {
doDraw(canvas); //真正繪製
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != canvas) {
mHolder.unlockCanvasAndPost(canvas);//結束鎖定畫圖,並提交改變
}
}
}
}
try {
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setRun(boolean isRun) {
this.mIsRun = isRun;
}
public void setStop() {
this.mIsStop = true;
}
}
}
複製代碼
完整的項目代碼: github.com/xinwei94/So…
看到這的大佬若是有時間且網速還行的話,幫忙點亮一下小星星~