话很少说,先上图为敬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…
看到这的大佬若是有时间且网速还行的话,帮忙点亮一下小星星~