Android波形动画

话很少说,先上图为敬java


产品经理:“两条曲线随声音大小变化而上下波动并向前滚动着”android

程序猿:“痛并快乐着”git


1、理论基础

ok 言归正传,做为一个理科生看到这条曲线首先想到的不该该是海浪、麻绳、马尾辫...而是正弦函数 y=sin(x)github


想要它滚动起来,那么就将图像向右平移 y=sin(x - k)canvas


当k值随着时间的变化而有序递增时,图像就会源源不断地向右运动啦。bash

另外,要知足曲线随声音大小变化而上下波动的需求,只须要调节 y=A*sin(x - k) 中的A值便可:ide


至此,数学理论基础描述完毕。函数

忍不住安利一下这个优美的函数图像绘制工具  www.desmos.com/calculator工具

2、实现原理

显然,安卓原生控件没法知足咱们这个需求,自能上自定义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;
}复制代码


3、完整代码

做为一个有追求的自定义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;
        }
    }
}
复制代码


4、结语

完整的项目代码: github.com/xinwei94/So…

看到这的大佬若是有时间且网速还行的话,帮忙点亮一下小星星~

相关文章
相关标签/搜索