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…

看到這的大佬若是有時間且網速還行的話,幫忙點亮一下小星星~

相關文章
相關標籤/搜索