也可看Android自绘控件开发与性能优化实践——以录音波浪动画为例

直接上代码
基类:

public abstract class RenderView extends SurfaceView implements SurfaceHolder.Callback {

    private static final String TAG = "RenderView";

    //是否正在绘制动画
    private boolean isStartAnim = false;
    private final static Object surfaceLock = new Object();
    private RenderThread renderThread;
    /**
     * 绘制背景,防止开始时黑屏
     * 子View可以执行此方法
     * @param canvas
     */
    protected abstract void doDrawBackground(Canvas canvas);
    /**
     * 渲染surfaceView的回调方法。
     *
     * @param canvas 画布
     */
    protected abstract void onRender(Canvas canvas, long millisPassed);

    public RenderView(Context context) {
        this(context, null);
    }

    public RenderView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RenderView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getHolder().addCallback(this);
    }


    /*回调/线程*/
    private static class RenderThread extends Thread {

        private static final long SLEEP_TIME = 16;
        private WeakReference<RenderView> renderView;
        private boolean running = false;
        private boolean destoryed = false;
        private boolean isPause = false;
        public RenderThread(RenderView renderView) {
            super("RenderThread");
            Log.e(TAG, "RenderThread: " );
            this.renderView = new WeakReference<>(renderView);
        }

        private SurfaceHolder getSurfaceHolder(){
            if (getRenderView() != null ){
                return getRenderView().getHolder();
            }
            return null;
        }

        private RenderView getRenderView(){
            return renderView.get();
        }

        @Override
        public void run() {
            long startAt = System.currentTimeMillis();
            while (!destoryed) {
//                Log.e("whileThread","RenderView动画线程222");
                synchronized (surfaceLock) {
                    //这里并没有真正的结束Thread,防止部分手机连续调用同一Thread出错
                    while (isPause){
                        try {
                            surfaceLock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    if (running) {
                        if (getSurfaceHolder() != null && getRenderView() != null) {
                            Canvas canvas = getSurfaceHolder().lockCanvas();
                            if (canvas != null) {
                                getRenderView().doDrawBackground(canvas);
                                if (getRenderView().isStartAnim) {
                                    //这里做真正绘制的事情
                                    getRenderView().render(canvas, System.currentTimeMillis() - startAt);
                                }
                                if(getSurfaceHolder().getSurface().isValid()){
                                    getSurfaceHolder().unlockCanvasAndPost(canvas);
                                }
                            }
                        }else {
                            running = false;
                        }

                    }

                }
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        }


        public void setRun(boolean isRun) {
            this.running = isRun;
        }

    }



    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        renderer = onCreateRenderer();
        if (renderer != null && renderer.isEmpty()) {
            throw new IllegalStateException();
        }

        renderThread = new RenderThread(this);
    }

    /**
     * 解锁暂停,继续执行绘制任务
     * 默认当Resume时不自动启动动画
     */
    public void onResume(){
        synchronized (surfaceLock){
            if (renderThread != null) {
                renderThread.isPause = false;
                surfaceLock.notifyAll();
            }
        }
    }


    //假暂停,并没有结束Thread
    public void onPause(){
        synchronized (surfaceLock){
            if (renderThread != null) {
                renderThread.isPause = true;
            }
        }
    }


    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        //这里可以获取SurfaceView的宽高等信息
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        synchronized (surfaceLock) {  //这里需要加锁,否则doDraw中有可能会crash
            renderThread.setRun(false);
            renderThread.destoryed = true;
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        if (hasFocus && isStartAnim){
            startAnim();
        }else {
            startThread();
        }
    }

    /*绘图*/
    public interface IRenderer {
        void onRender(Canvas canvas, long millisPassed);
    }

    private List<IRenderer> renderer;

    protected List<IRenderer> onCreateRenderer() {
        return null;
    }

    private void render(Canvas canvas, long millisPassed) {
        if (renderer != null) {
            for (int i = 0, size = renderer.size(); i < size; i++) {
                renderer.get(i).onRender(canvas, millisPassed);
            }
        } else {
            onRender(canvas, millisPassed);
        }
    }

    public void startAnim(){
        isStartAnim = true;
        startThread();
    }

    private void startThread(){

        if (renderThread != null && !renderThread.running) {
            renderThread.setRun(true);
            try {
                if (renderThread.getState() == Thread.State.NEW) {
                    renderThread.start();
                }

            }catch (RuntimeException e){
                e.printStackTrace();
            }

        }
    }

    public void stopAnim(){
        isStartAnim = false;
        if (renderThread != null && renderThread.running) {
            renderThread.setRun(false);
            renderThread.interrupt();
        }
    }

    public boolean isRunning(){
        if (renderThread != null) {
            return renderThread.running;
        }
        return false;
    }

    //释放相关资源,防止内存泄漏
    public void release(){
        if (getHolder() != null && getHolder().getSurface() != null) {
            getHolder().getSurface().release();
            getHolder().removeCallback(this);
        }
    }

}

真正的基于surfaceView的波浪动画view来了:

public class FakeVoiceViewWithSurface extends RenderView {

    private static final String TAG = "FakeVoiceViewWithSurface";

    private int mWidth;
    private int mHeight;

    private int lineColor = DEFAULT_LINE_COLOR;
    private float maxLineHeight;
    private int lineWidth = DEFAULT_LINE_WIDTH;
    private int lineGap = DEFAULT_LINE_GAP;
    //根据最后宽度来确定有几条跳动的线条
    private int lineCount;
    private float padding;

    private static final int DEFAULT_LINE_WIDTH = SizeUtils.dp2px(2);
    private static final int DEFAULT_LINE_COLOR = 0xFF38ABF3;
    private static final int DEFAULT_LINE_GAP = 10;


    final float DEFAULT_WIDTH = getContext().getResources().getDimension(R.dimen.qb_px_230);
    final float DEFAULT_HEIGHT = getContext().getResources().getDimension(R.dimen.qb_px_80);

    private Paint linePaint;

    private Paint rectPaint;
    private List<LineParamBean> lineList = new ArrayList<>();
    private RectF rectF;
    private boolean status;
    private Random random;
    private SurfaceHolder surfaceHolder;
    //    private boolean hadMeasured = false;
    private boolean isTransparentMode = false;

    //控制向右偏移速度,越小偏移速度越快
    private float offsetSpeed = 250F;

    private boolean isTrans = false;

    //背景色
    private int backGroundColor = Color.TRANSPARENT;

    public FakeVoiceViewWithSurface(Context context) {
        super(context);
    }

    public FakeVoiceViewWithSurface(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FakeVoiceViewWithSurface(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void doDrawBackground(Canvas canvas) {
        //绘制背景
        if(isTransparentMode) {
            //启用CLEAR模式,所绘制内容不会提交到画布上。
            canvas.drawColor(backGroundColor, PorterDuff.Mode.CLEAR);
        } else {
            canvas.drawColor(backGroundColor);
        }
    }

    @Override
    protected void onRender(Canvas canvas, long millisPassed) {
        if (isTrans) {
            canvas.drawColor(Color.WHITE);
            canvas.drawColor(backGroundColor, PorterDuff.Mode.SRC);
        }
        if(rectF == null || lineList.isEmpty()) {
            initDraw();
        }

        for(int i = 0; i < lineList.size(); i++) {
            LineParamBean bean = lineList.get(i);
            float offset = (millisPassed % bean.randomTime) / bean.randomTime;
            if(offset < 0.5f) {
                bean.height = 2 * offset * maxLineHeight;
            } else if(offset <= 1.0f) {
                bean.height = 2 * (1 - offset) * maxLineHeight;
            }
            bean.y = (mHeight - bean.height) / 2;
            if(!status) {
                bean.x = lineGap * (i + 1) + lineWidth * ((float) i + (1 / 2)) + padding;
            }
        }
        status = true;

        if (isTrans) rectPaint.setColor(backGroundColor);
        canvas.drawRoundRect(rectF, mHeight / 4, mHeight / 4, rectPaint);
        for(LineParamBean bean : lineList) {
            canvas.drawLine(bean.x, bean.y, bean.x, bean.y + bean.height, linePaint);
        }

    }

    public void setTrans(boolean trans) {
        isTrans = trans;
    }

    public boolean isStart() {
        return status;
    }

    private void initDraw() {
        this.mWidth = getMeasuredWidth();
        this.mHeight = getMeasuredHeight();
        Log.e(TAG, "width:" + mWidth);
        Log.e(TAG, "height:" + mHeight);

        padding = mWidth / 5;

        maxLineHeight = mHeight * 2 / 3;
        lineCount = (int) ((this.mWidth - lineGap - padding * 2) / (lineWidth + lineGap));

        lineList.clear();
        for(int i = 0; i < lineCount; i++) {
            LineParamBean bean = new LineParamBean();
            lineList.add(bean);
        }
        if(rectF == null) {
            rectF = new RectF();
        }
        rectF.set(0, 0, this.mWidth, this.mHeight);

    }

    private void init() {
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setDither(true);
        linePaint.setStrokeCap(Paint.Cap.ROUND);
        linePaint.setStrokeJoin(Paint.Join.ROUND);
        linePaint.setColor(lineColor);
        linePaint.setStrokeWidth(lineWidth);

        rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectPaint.setColor(0xFF2B2A32);
        rectPaint.setStyle(Paint.Style.FILL);
        random = new Random();
        initView();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = resolveSize((int) DEFAULT_WIDTH, widthMeasureSpec);
        int height = resolveSize((int) DEFAULT_HEIGHT, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }


    private void stopAllAnim() {
        status = false;
    }

    public void releaseAnimator() {
        stopAllAnim();
//        clearAll();
    }

    private void clearAll() {
        //防止内存泄漏
        lineList.clear();
    }

    @Override
    public void startAnim() {
        super.startAnim();
    }

    @Override
    public void stopAnim() {
        super.stopAnim();
        stopAllAnim();
        clearDraw();
    }

    //清空画布所有内容
    private void clearDraw() {
        Canvas canvas = null;
        try {
            canvas = getHolder().lockCanvas(null);
            canvas.drawColor(backGroundColor);
            clearAll();
        } catch(Exception e) {
        } finally {
            if(canvas != null) {
                getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

    /**
     * 初始化View
     */
    private void initView() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        setZOrderOnTop(true);
        surfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
        setFocusable(true);
        setKeepScreenOn(true);
        setFocusableInTouchMode(true);
    }

}

使用也简单,直接在布局xml使用即可。
然后在对应的Fragment或Activity,需要启动动画的地方,调用接口方法startAnim(),在需要停止动画的地方调用接口方法stopAnim()即可。

以下是另一种的波浪动画:

public class WaveLineView extends RenderView {

    private final int DEFAULT_SAMPLING_SIZE = 64;
    private final float DEFAULT_OFFSET_SPEED = 250F;
    private final int DEFAULT_SENSIBILITY = 5;

    //采样点的数量,越高越精细,但是高于一定限度肉眼很难分辨,越高绘制效率越低
    private int samplingSize;

    //控制向右偏移速度,越小偏移速度越快
    private float offsetSpeed;
    //平滑改变的音量值
    private float volume = 0;

    //用户设置的音量,[0,100]
    private int targetVolume = 50;

    //每次平滑改变的音量单元
    private float perVolume;

    //灵敏度,越大越灵敏[1,10]
    private int sensibility;

    //背景色
    private int backGroundColor = Color.WHITE;

    //波浪线颜色
    private int lineColor;
    //粗线宽度
    private int thickLineWidth;
    //细线宽度
    private int fineLineWidth;

    private final Paint paint = new Paint();
    private RectF rectF;
    private Paint bgPaint;

    {
        //防抖动
        paint.setDither(true);
        //抗锯齿,降低分辨率,提高绘制效率
        paint.setAntiAlias(true);
    }

    private List<Path> paths = new ArrayList<>();

    {
        for(int i = 0; i < 4; i++) {
            paths.add(new Path());
        }
    }

    //不同函数曲线系数
    private float[] pathFuncs = {0.6f, 0.35f, 0.1f, -0.1f};

    //采样点X坐标
    private float[] samplingX;
    //采样点位置映射到[-2,2]之间
    private float[] mapX;
    //画布宽高
    private int width, height;
    //画布中心的高度
    private int centerHeight;
    //振幅
    private float amplitude;
    //存储衰变系数
    private SparseArray<Double> recessionFuncs = new SparseArray<>();
    //连线动画结束标记
    private boolean isPrepareLineAnimEnd = false;
    //连线动画位移
    private int lineAnimX = 0;
    //渐入动画结束标记
    private boolean isPrepareAlphaAnimEnd = false;
    //渐入动画百分比值[0,1f]
    private float prepareAlpha = 0f;
    //是否开启准备动画
    private boolean isOpenPrepareAnim = false;

    private boolean isTransparentMode = false;

    public WaveLineView(Context context) {
        this(context, null);
    }

    public WaveLineView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaveLineView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(attrs);
    }

    private void initAttr(AttributeSet attrs) {
        TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.WaveLineView);
        backGroundColor = t.getColor(R.styleable.WaveLineView_wlvBackgroundColor, Color.WHITE);
        samplingSize = t.getInt(R.styleable.WaveLineView_wlvSamplingSize, DEFAULT_SAMPLING_SIZE);
        lineColor = t.getColor(R.styleable.WaveLineView_wlvLineColor, Color.parseColor("#2ED184"));
        thickLineWidth = (int) t.getDimension(R.styleable.WaveLineView_wlvThickLineWidth, 6);
        fineLineWidth = (int) t.getDimension(R.styleable.WaveLineView_wlvFineLineWidth, 2);
        offsetSpeed = t.getFloat(R.styleable.WaveLineView_wlvMoveSpeed, DEFAULT_OFFSET_SPEED);
        sensibility = t.getInt(R.styleable.WaveLineView_wlvSensibility, DEFAULT_SENSIBILITY);
        isTransparentMode = backGroundColor == Color.TRANSPARENT;
        t.recycle();
        checkVolumeValue();
        checkSensibilityValue();
        //将RenderView放到最顶层
        setZOrderOnTop(true);
        if(getHolder() != null) {
            //使窗口支持透明度
            getHolder().setFormat(PixelFormat.TRANSLUCENT);
        }
        rectF = new RectF();
        bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    @Override
    protected void doDrawBackground(Canvas canvas) {
        //绘制背景
        if(isTransparentMode) {
            //启用CLEAR模式,所绘制内容不会提交到画布上。
            canvas.drawColor(backGroundColor, PorterDuff.Mode.CLEAR);
        } else {
            bgPaint.setColor(backGroundColor);
            rectF.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
            canvas.drawRoundRect(rectF, getMeasuredHeight() / 4, getMeasuredHeight() / 4, bgPaint);
        }
    }

    @Override
    public void release() {
        super.release();
    }

    @Override
    protected void onRender(Canvas canvas, long millisPassed) {
        float offset = millisPassed / offsetSpeed;

        if(null == samplingX || null == mapX || null == pathFuncs) {
            initDraw(canvas);
        }

        if(lineAnim(canvas)) {
            resetPaths();
            softerChangeVolume();

            //波形函数的值
            float curY;
            for(int i = 0; i <= samplingSize; i++) {
                float x = samplingX[i];
                curY = (float) (amplitude * calcValue(mapX[i], offset));
                for(int n = 0; n < paths.size(); n++) {
                    //四条线分别乘以不同的函数系数
                    float realY = curY * pathFuncs[n] * volume * 0.01f;
                    paths.get(n).lineTo(x, centerHeight + realY);
                }
            }

            //连线至终点
            for(int i = 0; i < paths.size(); i++) {
                paths.get(i).moveTo(width, centerHeight);
            }

            //绘制曲线
            for(int n = 0; n < paths.size(); n++) {

                if(n == 0) {
                    paint.setStrokeWidth(thickLineWidth);
                    paint.setAlpha((int) (255 * alphaInAnim()));
                } else {
                    paint.setStrokeWidth(fineLineWidth);
                    paint.setAlpha((int) (100 * alphaInAnim()));
                }
                canvas.drawPath(paths.get(n), paint);
            }

        }

    }

    //检查音量是否合法
    private void checkVolumeValue() {
        if(targetVolume > 100) {
            targetVolume = 100;
        }
    }

    //检查灵敏度值是否合法
    private void checkSensibilityValue() {
        if(sensibility > 10) {
            sensibility = 10;
        }
        if(sensibility < 1) {
            sensibility = 1;
        }
    }

    /**
     * 使曲线振幅有较大改变时动画过渡自然
     */
    private void softerChangeVolume() {
        //这里减去perVolume是为了防止volume频繁在targetVolume上下抖动
        if(volume < targetVolume - perVolume) {
            volume += perVolume;
        } else if(volume > targetVolume + perVolume) {
            if(volume < perVolume * 2) {
                volume = perVolume * 2;
            } else {
                volume -= perVolume;
            }
        } else {
            volume = targetVolume;
        }

    }

    /**
     * 渐入动画
     *
     * @return progress of animation
     */
    private float alphaInAnim() {
        if(!isOpenPrepareAnim) {
            return 1;
        }
        if(prepareAlpha < 1f) {
            prepareAlpha += 0.02f;
        } else {
            prepareAlpha = 1;
        }
        return prepareAlpha;
    }

    /**
     * 连线动画
     *
     * @param canvas
     * @return whether animation is end
     */
    private boolean lineAnim(Canvas canvas) {
        if(isPrepareLineAnimEnd || !isOpenPrepareAnim) {
            return true;
        }
        paths.get(0).moveTo(0, centerHeight);
        paths.get(1).moveTo(width, centerHeight);

        for(int i = 1; i <= samplingSize; i++) {
            float x = i * lineAnimX / samplingSize;
            paths.get(0).lineTo(x, centerHeight);
            paths.get(1).lineTo(width - x, centerHeight);

        }

        paths.get(0).moveTo(width / 2, centerHeight);
        paths.get(1).moveTo(width / 2, centerHeight);

        lineAnimX += width / 60;
        canvas.drawPath(paths.get(0), paint);
        canvas.drawPath(paths.get(1), paint);

        if(lineAnimX > width / 2) {
            isPrepareLineAnimEnd = true;
            return true;
        }
        return false;
    }

    /**
     * 重置path
     */
    private void resetPaths() {
        for(int i = 0; i < paths.size(); i++) {
            paths.get(i).rewind();
            paths.get(i).moveTo(0, centerHeight);
        }
    }

    //初始化参数
    private void initParameters() {
        lineAnimX = 0;
        prepareAlpha = 0f;
        isPrepareLineAnimEnd = false;
        isPrepareAlphaAnimEnd = false;
        samplingX = null;
    }

    @Override
    public void startAnim() {
        initParameters();
        super.startAnim();
    }

    @Override
    public void stopAnim() {
        super.stopAnim();
        clearDraw();
    }

    //清空画布所有内容
    public void clearDraw() {
        Canvas canvas = null;
        try {
            canvas = getHolder().lockCanvas(null);
            canvas.drawColor(backGroundColor);
            resetPaths();
            for(int i = 0; i < paths.size(); i++) {
                canvas.drawPath(paths.get(i), paint);
            }
        } catch(Exception e) {
        } finally {
            if(canvas != null) {
                getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

    //初始化绘制参数
    private void initDraw(Canvas canvas) {

        width = canvas.getWidth();
        height = canvas.getHeight();
        centerHeight = height >> 1;
        //振幅为高度的1/4
        amplitude = height / 3.0f;

        //适合View的理论最大音量值,和音量不属于同一概念
        perVolume = sensibility * 0.35f;

        //初始化采样点及映射
        //这里因为包括起点和终点,所以需要+1
        samplingX = new float[samplingSize + 1];
        mapX = new float[samplingSize + 1];
        //确定采样点之间的间距
        float gap = width / (float) samplingSize;
        //采样点的位置
        float x;
        for(int i = 0; i <= samplingSize; i++) {
            x = i * gap;
            samplingX[i] = x;
            //将采样点映射到[-2,2]
            mapX[i] = (x / (float) width) * 4 - 2;
        }

        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(lineColor);
        paint.setStrokeWidth(thickLineWidth);
    }

    /**
     * 计算波形函数中x对应的y值
     * <p>
     * 使用稀疏矩阵进行暂存计算好的衰减系数值,下次使用时直接查找,减少计算量
     *
     * @param mapX   换算到[-2,2]之间的x值
     * @param offset 偏移量
     * @return [-1, 1]
     */
    private double calcValue(float mapX, float offset) {
        int keyX = (int) (mapX * 1000);
        offset %= 2;
        double sinFunc = Math.sin(Math.PI * mapX - offset * Math.PI);
        double recessionFunc;
        if(recessionFuncs.indexOfKey(keyX) >= 0) {
            recessionFunc = recessionFuncs.get(keyX);
        } else {
            recessionFunc = 4 / (4 + Math.pow(mapX, 4));
            recessionFuncs.put(keyX, recessionFunc);
        }
        return sinFunc * recessionFunc;
    }

    /**
     * the wave line animation move speed from left to right
     * you can use negative number to make the animation from right to left
     * the default value is 290F,the smaller, the faster
     *
     * @param moveSpeed
     */
    public void setMoveSpeed(float moveSpeed) {
        this.offsetSpeed = moveSpeed;
    }


    /**
     * User set volume, [0,100]
     *
     * @param volume
     */
    public void setVolume(int volume) {
        if(Math.abs(targetVolume - volume) > perVolume) {
            this.targetVolume = volume;
            checkVolumeValue();
        }
    }

    public void setBackGroundColor(int backGroundColor) {
        this.backGroundColor = backGroundColor;
    }

    public void setLineColor(int lineColor) {
        this.lineColor = lineColor;
    }

    /**
     * Sensitivity, the bigger the more sensitive [1,10]
     * the default value is 5
     *
     * @param sensibility
     */
    public void setSensibility(int sensibility) {
        this.sensibility = sensibility;
        checkSensibilityValue();
    }
}