使用surfaceView结合贝塞尔曲线生成波形动态控件

本文介绍了一种基于SurfaceView的波形动态控件绘制方法,利用2阶贝塞尔曲线实现波浪效果,并通过调整控制点使波浪形状变化。通过多层叠加不同颜色的波形,营造出丰富的视觉效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



首先,使用surfaceViw是为了让控件减少主线程资源的占用。考虑到该控件有可能是长时间在当前界面上运行,而且该控件是处于一直刷新的状态,如果使用普通view,会占用主线程的资源,造成界面卡顿;

波形动态控件的波形实现是先使用路径绘制多个波形封闭形状,并填充半透明颜色,然后将这多个封闭形状叠加在一起。

(图1)

图1

波形封闭形状使用Path来绘制,先绘制上面的曲线,然后依次绘制==>A.右边的竖直线==>B.底部直线=>C左边竖直线。
AB两条竖直线使用Path的lineTo方法绘制,最后的C调用Path的close方法,表示将这条路径封闭。

上面的曲线绘制就要使用到Path的quadTo方法了,这个方法就是绘制2阶贝塞尔曲线,3阶贝塞尔曲线使用cubicTo方法。这里用quadTo就可以了。

2阶,3阶贝塞尔曲线大概可以这么理解:

2阶:一条线,首尾两端固定,然后中间取一个控制点,然后拉动这个点,使这条线变弯曲。(实际情况是控制点不是在曲线上)

(图2)

图2

3阶:一条线,首尾两端固定,然后中间取两个点,然后拉动这个两个控制点,使这条线变弯曲。(实际情况是控制点不是在曲线上)

(图3)

这里写图片描述

然后,为什么开始坐标,结束坐标,控制点坐标都一样,生成的曲线就一定是A的形状呢,这个要从贝塞尔曲线的原理说起

这里写图片描述

二阶贝塞尔曲线:

一个二阶贝塞尔曲线需要一个开始点,控制点,结束点,这三点,对应到图上就是P1,P2,P3
把这三个点用两条直线L1,L2连接起来,

我们要知道曲线可以认为是无数个点连接起来的.接下来就可以取点绘制曲线了,
第一个点:先取L1的0%的位置和L2的0%的位置连接一个直线L3,然后在L3的0%位置取一个点,后面曲线经过这个点时会和这个点相切
第二个点:先取L1的10%的位置和L2的10%的位置连接一个直线L3,然后在L3的10%位置取一个点,后面曲线经过这个点时会和这个点相切
第三个点:先取L1的20%的位置和L2的20%的位置连接一个直线L3,然后在L3的20%位置取一个点,后面曲线经过这个点时会和这个点相切

这个过程是我自己的理解,公式什么的起始我也不懂,只是看下面参考网址的动图推测的,建议大家进去看一下,人家写的比我专业多了,也有动图帮助理解.
这样直到100%,这根2阶贝塞尔曲线就画出来了(图中的辅助线是到50%时候的情况).
这里写图片描述

然后3阶,4阶级也都是用同样的方法,只不过线和点的数量增加了(图中的辅助线是到50%时候的情况).

具体计算公式和详细说明可参考 http://www.cnblogs.com/hnfxs/p/3148483.html
这里写图片描述



了解这个之后就可以开始画图形了,图形绘制如下步骤:

(图4)

图4

图形划出来了,接下来就要让这个波形怎么让他动起来,这个很简单,只要让每次刷新的时候,然这个图形向后移动一点,
这也是为什么上面绘制图形的时候要先绘制到屏幕外面。这样看起来就像波形在动了。

(图5)

图5

接下来看代码

首先,创建一个类,继承SurfaceView,并实现SurfaceHolder.Callback和Runnable

class JonsonWaveView extends SurfaceView implements SurfaceHolder.Callback , Runnable  

然后在这个类的构造器初始化方法里面做以下操作

    //获取屏幕密度
    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(displayMetrics);
    mDensity = displayMetrics.density;

    //获取xml文件中填写的属性
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.JonsonWaveView);
    xmlWaveHeightIndex = typedArray.getDimension(R.styleable.JonsonWaveView_waveHeight , 300f);//波形位置(水位高度)
    xmlSwingHeight = typedArray.getDimension(R.styleable.JonsonWaveView_swingHeight, 10f);//波形波动的高度(波峰到基线的高度)
    xmlWave1Color = typedArray.getColor(R.styleable.JonsonWaveView_wave1Color , Color.parseColor("#BFE5FF"));//第一层形状颜色
    xmlWave2Color = typedArray.getColor(R.styleable.JonsonWaveView_wave2Color , Color.parseColor("#7AE5FF"));//第二层形状颜色
    xmlWave3Color = typedArray.getColor(R.styleable.JonsonWaveView_wave3Color , Color.parseColor("#0CE5FF"));//第三层形状颜色
    xmlWave2Range = typedArray.getFloat(R.styleable.JonsonWaveView_wave3Range , 15f);//第二层和第一层的距离
    xmlWave3Range = typedArray.getFloat(R.styleable.JonsonWaveView_wave2Range , 20f);//第三层第一层的距离
    xmlBgStartColor = typedArray.getColor(R.styleable.JonsonWaveView_startColor, Color.WHITE);//背景为渐变色,渐变色的开始颜色
    xmlBgEndColor = typedArray.getColor(R.styleable.JonsonWaveView_endColor, Color.parseColor("#C8E5FF"));//背景为渐变色,渐变色的结束颜色
    xmlWaveSpeed = typedArray.getFloat(R.styleable.JonsonWaveView_waveSpeed, 1.5f);//波形流动的速度
    cyclerCount = typedArray.getInteger(R.styleable.JonsonWaveView_cyclerCount, 2);//控件显示多少个周期

    mHolder = getHolder();
    mHolder.addCallback(this);//获取SurfaceHolder并添加回调

    //初始化波浪画笔
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setColor(xmlWave1Color);
    mPaint.setStyle(Paint.Style.FILL);

    //初始化背景画笔
    mBgPaint = new Paint();
    mBgPaint.setStyle(Paint.Style.FILL);  


下面的这两行代码是取出SurfaceView中的holder,然后这个holder需要给他设置一个SurfaceHolder.Callback.

mHolder = getHolder();
mHolder.addCallback(this);  


这个callback会回调以下方法:
1.surfaceCreated(surfaceView 初始化完成后调用)
2.surfaceChanged
3.surfaceDestroyed(surfaceView被关闭时调用)

我们的JonsonWaveView已经实现了这个接口,所以用this即可.


构造器执行完成后,接下来就会执行到回调surfaceCreated

sufaceCreated方法的内容如下:

@Override
public void surfaceCreated(SurfaceHolder holder) {
    //是否可以绘制的标志
    mIsDrawing = true;

    //获取控件宽高
    width = getWidth();
    height = getHeight();


    //初始化渐变,给背景色画笔设置渐变
    LinearGradient linearGradient = new LinearGradient(0, 0, 0, getHeight() , new int[] {xmlBgStartColor, xmlBgEndColor}, null, Shader.TileMode.MIRROR);
    mBgPaint.setShader(linearGradient);

    //第一层波形底部高度
    float shape1Top = getHeight() - xmlWaveHeightIndex;//第一层形状的top位置

    swingHeight = (int) (xmlSwingHeight * mDensity);//波动高度(波峰到基线高度)
    layerRange1 = (int) (shape1Top + xmlSwingHeight);//第一层图形贝塞尔曲线基线高度
    layerRange2 = (int) (xmlWave2Range * mDensity + layerRange1);//第二层图形贝塞尔曲线基线高度
    layerRange3 = (int) (xmlWave3Range * mDensity + layerRange1);//第三层图形贝塞尔曲线基线高度

    cycleWidth = width / cyclerCount;//一个波动周期宽度
    besselWidth = cycleWidth / 2;//半个波动周期宽度(一个2阶贝塞尔曲线的宽度)

    new Thread(this).start();//执行子线程中的任务(绘制图形)
}  

首先设置一个mIsDrawing标志,这个标志用于控制是否要绘制.
这个主要是防止surfaceView不在当前显示后会闪退的问题,因为设置路径和绘制图形的时候,是在子线程中执行的,
如果surfaceView不显示的时候,子线程绘制未完成,那么等子线程完成后,它就会把完成绘制的画板提交到主线程显示,
这时候由于surfaceView控件已经不显示了,而子线程又要主线程来显示不存在的控件,这个就会导致崩溃.
所以在回调surfaceDestroyed的时候,这个标志会被设置为false.


接下来获取控件宽高尺寸,初始化背景渐变色,各项尺寸,留着备用.
最后一行new Thread(this).start(),这个就是开始执行绘制图形任务了.绘制图形任务代码如下:(绘制顺序可以参照上面的图4)

@Override
public void run() {
    while(mIsDrawing){

        //第一层图形路径
        mPath = new Path();
        mPath.moveTo(-cycleWidth + offset , layerRange1);//路径移动起始点到负一个周期的位置,加上当前时间的图形移动距离

        mPath.quadTo(-cycleWidth + besselWidth / 2 + offset , layerRange1 + swingHeight , -cycleWidth / 2 + offset , layerRange1);
        mPath.quadTo(-cycleWidth / 2 + besselWidth /2 + offset , layerRange1 - swingHeight , offset , layerRange1);

        for (int i = 0; i < cyclerCount; i++) {
            //quadTo
            //参数1:贝塞尔曲线控制点X坐标
            //参数2:贝塞尔曲线控制点Y坐标
            //参数3:贝塞尔曲线终点X坐标
            //参数4:贝塞尔曲线终点y坐标
            float controlX = (i * cycleWidth) + (besselWidth / 2) + offset;
            float controlY = layerRange1 + swingHeight;
            float endX = (cycleWidth * i) + besselWidth + offset;
            float endY = layerRange1;
            mPath.quadTo(controlX , controlY , endX , endY);//第i个周期内的第一个贝塞尔曲线

            float controlX1 = endX + (besselWidth/2);
            float controlY1 = layerRange1 - swingHeight;
            float endX1 = endX + besselWidth;
            float endY1 = endY;
            mPath.quadTo(controlX1 , controlY1 , endX1 , endY1);//第i个周期内的第二个贝塞尔曲线
            if(i == cyclerCount - 1){
                mPath.lineTo(width , height);//路径-右边竖直线
                mPath.lineTo(-cycleWidth , height);//路径-底部横直线
                mPath.close();//路径-封闭
            }
        }


        //第二层图形路径
        ...(同上)
        //第三层图形路径
        ...(同上)

        //递增图形的移动量;
        offset += (int) (2  * xmlWaveSpeed* mDensity);//图形1的偏移量
        offset1 += (int) (1 * xmlWaveSpeed * mDensity);//图形2的偏移量
        offset2 += (int) (1.5 * xmlWaveSpeed * mDensity);//图形3的偏移量

        draw();//路径创建按完成后就可以开始按照路径来绘制了。
    }
}

绘制图形路径,首先
先创建一个路径(Path)->
把路径移动到图形的左上角->

绘制预备周期:

预备周期的第一条曲线->
预备周期的第一条曲线->

绘制正式周期:

for循环周期数,一共要绘制多少个周期通过cyclerCount控制,绘制周期的时候加上偏移量
(偏移量是控制图形移动的关键)

接下来用同样的方法绘制第二层,第三层路径.
然后递增每一个图形的偏移量,为下一次的绘制做准备
然后调用绘制方法draw();

绘制方法draw()的代码如下:

private void draw(){
    try {
        mCanvas = mHolder.lockCanvas();//获取画板,

        mCanvas.drawColor(Color.WHITE);//清屏

        mCanvas.drawRect(0, 0, getWidth(), getHeight(), mBgPaint);//绘制控件的底色


        mPaint.setColor(xmlWave1Color);//开始绘制第一层图形,设置画笔颜色为第一层的颜色
        mCanvas.drawPath(mPath, mPaint);//绘制第一层图形路径
        mPaint.setColor(xmlWave2Color);//开始绘制第二层图形,设置画笔颜色为第二层的颜色
        mCanvas.drawPath(mPath1 , mPaint);//绘制第二层图形路径
        mPaint.setColor(xmlWave3Color);//开始绘制第三层图形,设置画笔颜色为第三层的颜色
        mCanvas.drawPath(mPath2 , mPaint);//绘制第三层图形路径


        //判移动移量是否已经超过预备周期的宽度,是的话把偏移量重置为0
        if (offset > cycleWidth) {
            offset = 0;
        }
        if (offset1 > cycleWidth) {
            offset1 = 0;
        }
        if (offset2 > cycleWidth) {
            offset2 = 0;
        }
    }catch (Exception e){
    }finally {
        /*mCanvas != null 是因为在华为4.4上,跳转Activity后,mCanvas会变成空,返回的时候有可能报空指针异常*/
        if(mIsDrawing && mCanvas != null){
            mHolder.unlockCanvasAndPost(mCanvas);//解锁,把绘制的内容提交到屏幕上
        }
    }

}  

holder.lockCanvas方法返回一个画板,显示的内容最终就要绘制到该画板上,并提交给主线程显示.
然后需要先把上次画板上绘制的内容清除掉,所以这里直接给画板绘制一个铺满整个画板的颜色,这样来覆盖掉原来的内容;
然后按照层叠顺序,先绘制底色,再绘制后面的三个波浪图形.
最后的finally,确保绘制的内容都会执行提交,注意,在华为4.4上,跳转Activity后,mCanvas会变成空,这里要加个判断.

提交以后,绘制的内容就会显示到屏幕上啦.由于run()方法中执行的是 while(mIsDrawing),所以在mIsDrawing等于false之前,他都会一直执行绘制,递增偏移量.这样就可以实现动画的效果.

最终这个控件用在了17做网店 App的登录界面上.

最后这个控件的使用方法如下:
1.先把JonsonWAveView复制到项目中,
2.在src/main/values文件夹下的attrs中添加以下自定义属性:

<declare-styleable name="JonsonWaveView">
    <attr name="waveHeight" format="dimension"/><!--波形位置(水位高度)-->
    <attr name="swingHeight" format="dimension"/><!--波形波动的高度(波峰到基线的高度)-->
    <attr name="wave1Color" format="color"/><!--第一层形状颜色-->
    <attr name="wave2Color" format="color"/><!--第二层形状颜色-->
    <attr name="wave3Color" format="color"/><!--第三层形状颜色-->
    <attr name="wave2Range" format="dimension"/><!--第二层和第一层的距离-->
    <attr name="wave3Range" format="dimension"/><!--第三层第一层的距离-->
    <attr name="startColor" format="color"/><!--背景为渐变色,渐变色的开始颜色-->
    <attr name="endColor" format="color"/><!--背景为渐变色,渐变色的结束颜色-->
    <attr name="waveSpeed" format="float"/><!--波形流动的速度-->
    <attr name="cyclerCount" format="integer"/><!--控件显示多少个周期-->
</declare-styleable>

这样在xml中的根标签添加xmlns:jonson_waveView="http://schemas.android.com/apk/res-auto"就可以使用这些属性了.

后面,贴上整个java文件的代码


public class JonsonWaveView extends SurfaceView implements SurfaceHolder.Callback , Runnable {

    private SurfaceHolder mHolder;
    private boolean mIsDrawing = false;
    private int width;
    private int height;
    private float mDensity;
    private Paint mPaint;
    private Canvas mCanvas;
    private int layerRange1;
    private int layerRange2;
    private int layerRange3;
    private Paint mBgPaint;
    private float xmlWaveHeightIndex;
    private float xmlSwingHeight;
    private int xmlBgStartColor;
    private int xmlBgEndColor;
    private float xmlWaveSpeed;
    private int xmlWave1Color;
    private int xmlWave2Color;
    private int xmlWave3Color;
    private float xmlWave2Range;
    private float xmlWave3Range;
    private int cyclerCount;

    private int offset = 0;
    private int offset1 = 0;
    private int offset2 = 0;

    int swingHeight;
    int cycleWidth;
    int besselWidth;

    Path mPath;
    Path mPath1;
    Path mPath2;


    public JonsonWaveView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context , attrs);
    }

    public JonsonWaveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context , attrs);
    }

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

    private void initView(Context context , AttributeSet attrs){

        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics displayMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(displayMetrics);
        mDensity = displayMetrics.density;

        //获取xml文件中填写的属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.JonsonWaveView);
        xmlWaveHeightIndex = typedArray.getDimension(R.styleable.JonsonWaveView_waveHeight , 300f);//波形位置(水位高度)
        xmlSwingHeight = typedArray.getDimension(R.styleable.JonsonWaveView_swingHeight, 10f);//波形波动的高度(波峰到基线的高度)
        xmlWave1Color = typedArray.getColor(R.styleable.JonsonWaveView_wave1Color , Color.parseColor("#BFE5FF"));//第一层形状颜色
        xmlWave2Color = typedArray.getColor(R.styleable.JonsonWaveView_wave2Color , Color.parseColor("#7AE5FF"));//第二层形状颜色
        xmlWave3Color = typedArray.getColor(R.styleable.JonsonWaveView_wave3Color , Color.parseColor("#0CE5FF"));//第三层形状颜色
        xmlWave2Range = typedArray.getFloat(R.styleable.JonsonWaveView_wave3Range , 15f);//第二层和第一层的距离
        xmlWave3Range = typedArray.getFloat(R.styleable.JonsonWaveView_wave2Range , 20f);//第三层第一层的距离
        xmlBgStartColor = typedArray.getColor(R.styleable.JonsonWaveView_startColor, Color.WHITE);//背景为渐变色,渐变色的开始颜色
        xmlBgEndColor = typedArray.getColor(R.styleable.JonsonWaveView_endColor, Color.parseColor("#C8E5FF"));//背景为渐变色,渐变色的结束颜色
        xmlWaveSpeed = typedArray.getFloat(R.styleable.JonsonWaveView_waveSpeed, 1.5f);//波形流动的速度
        cyclerCount = typedArray.getInteger(R.styleable.JonsonWaveView_cyclerCount, 2);//控件显示多少个周期

        mHolder = getHolder();
        mHolder.addCallback(this);//获取SurfaceHolder并添加回调

        //初始化波浪画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(xmlWave1Color);
        mPaint.setStyle(Paint.Style.FILL);

        //初始化背景画笔
        mBgPaint = new Paint();
        mBgPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        //是否可以绘制的标志
        mIsDrawing = true;

        //获取控件宽高
        width = getWidth();
        height = getHeight();


        //初始化渐变,给背景色画笔设置渐变
        LinearGradient linearGradient = new LinearGradient(0, 0, 0, getHeight() , new int[] {xmlBgStartColor, xmlBgEndColor}, null, Shader.TileMode.MIRROR);
        mBgPaint.setShader(linearGradient);

        //第一层波形底部高度
        float shape1Top = getHeight() - xmlWaveHeightIndex;//第一层形状的top位置

        swingHeight = (int) (xmlSwingHeight * mDensity);//波动高度(波峰到基线高度)
        layerRange1 = (int) (shape1Top + xmlSwingHeight);//第一层图形贝塞尔曲线基线高度
        layerRange2 = (int) (xmlWave2Range * mDensity + layerRange1);//第二层图形贝塞尔曲线基线高度
        layerRange3 = (int) (xmlWave3Range * mDensity + layerRange1);//第三层图形贝塞尔曲线基线高度

        cycleWidth = width / cyclerCount;//一个波动周期宽度
        besselWidth = cycleWidth / 2;//半个波动周期宽度(一个2阶贝塞尔曲线的宽度)

        new Thread(this).start();//执行子线程中的任务
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while(mIsDrawing){

            //第一层图形路径
            mPath = new Path();
            mPath.moveTo(-cycleWidth + offset , layerRange1);//路径移动起始点到负一个周期的位置,加上当前时间的图形移动距离

            mPath.quadTo(-cycleWidth + besselWidth / 2 + offset , layerRange1 + swingHeight , -cycleWidth / 2 + offset , layerRange1);
            mPath.quadTo(-cycleWidth / 2 + besselWidth /2 + offset , layerRange1 - swingHeight , offset , layerRange1);

            for (int i = 0; i < cyclerCount; i++) {
                //quadTo
                //参数1:贝塞尔曲线控制点X坐标
                //参数2:贝塞尔曲线控制点Y坐标
                //参数3:贝塞尔曲线终点X坐标
                //参数4:贝塞尔曲线终点y坐标
                float controlX = (i * cycleWidth) + (besselWidth / 2) + offset;
                float controlY = layerRange1 + swingHeight;
                float endX = (cycleWidth * i) + besselWidth + offset;
                float endY = layerRange1;

                mPath.quadTo(controlX , controlY , endX , endY);

                float controlX1 = endX + (besselWidth/2);
                float controlY1 = layerRange1 - swingHeight;
                float endX1 = endX + besselWidth;
                float endY1 = endY;
                mPath.quadTo(controlX1 , controlY1 , endX1 , endY1);
                if(i == cyclerCount - 1){
                    mPath.lineTo(width , height);//路径-右边竖直线
                    mPath.lineTo(-cycleWidth , height);//路径-底部横直线
                    mPath.close();//路径-封闭
                }
            }


            //第二层图形路径
            mPath1 = new Path();
            mPath1.moveTo(-cycleWidth + offset1 , layerRange2);//路径移动起始点到负一个周期的位置,加上当前时间的图形移动距离

            mPath1.quadTo(-cycleWidth + besselWidth / 2 + offset1 , layerRange2 + swingHeight , -cycleWidth / 2 + offset1 , layerRange2);
            mPath1.quadTo(-cycleWidth / 2 + besselWidth /2 + offset1 , layerRange2 - swingHeight , offset1 , layerRange2);

            for (int i = 0; i < cyclerCount; i++) {
                float controlX = (i * cycleWidth) + (besselWidth / 2) + offset1;
                float controlY = layerRange2 + swingHeight;
                float endX = (cycleWidth * i) + besselWidth + offset1;
                float endY = layerRange2;

                mPath1.quadTo(controlX , controlY , endX , endY);

                float controlX1 = endX + (besselWidth/2);
                float controlY1 = layerRange2 - swingHeight;
                float endX1 = endX + besselWidth;
                float endY1 = endY;
                mPath1.quadTo(controlX1 , controlY1 , endX1 , endY1);
                if(i == cyclerCount - 1){
                    mPath1.lineTo(width , height);//路径-右边竖直线
                    mPath1.lineTo(-cycleWidth , height);//路径-底部横直线
                    mPath1.close();//路径-封闭
                }
            }

            //第三层图形路径
            mPath2 = new Path();
            mPath2.moveTo(-cycleWidth + offset2 , layerRange3);//路径移动起始点到负一个周期的位置,加上当前时间的图形移动距离

            mPath2.quadTo(-cycleWidth + besselWidth / 2 + offset2 , layerRange3 + swingHeight , -cycleWidth / 2 + offset2 , layerRange3);
            mPath2.quadTo(-cycleWidth / 2 + besselWidth /2 + offset2 , layerRange3 - swingHeight , offset2 , layerRange3);

            for (int i = 0; i < cyclerCount; i++) {
                float controlX = (i * cycleWidth) + (besselWidth / 2) + offset2;
                float controlY = layerRange3 + swingHeight;
                float endX = (cycleWidth * i) + besselWidth + offset2;
                float endY = layerRange3;

                mPath2.quadTo(controlX , controlY , endX , endY);

                float controlX1 = endX + (besselWidth/2);
                float controlY1 = layerRange3 - swingHeight;
                float endX1 = endX + besselWidth;
                float endY1 = endY;
                mPath2.quadTo(controlX1 , controlY1 , endX1 , endY1);
                if(i == cyclerCount - 1){
                    mPath2.lineTo(width , height);//路径-右边竖直线
                    mPath2.lineTo(-cycleWidth , height);//路径-底部横直线
                    mPath2.close();//路径-封闭
                }
            }

            //递增图形的移动量;
            offset += (int) (2  * xmlWaveSpeed* mDensity);
            offset1 += (int) (1 * xmlWaveSpeed * mDensity);
            offset2 += (int) (1.5 * xmlWaveSpeed * mDensity);

            draw();//路径创建按完成后就可以开始按照路径来绘制了。
        }
    }

    private void draw(){
        try {
            mCanvas = mHolder.lockCanvas();//锁住画板,

            mCanvas.drawColor(Color.WHITE);//清屏

            mCanvas.drawRect(0, 0, getWidth(), getHeight(), mBgPaint);//绘制控件的底色


            mPaint.setColor(xmlWave1Color);//开始绘制第一层图形,设置画笔颜色为第一层的颜色
            mCanvas.drawPath(mPath, mPaint);//绘制第一层图形路径
            mPaint.setColor(xmlWave2Color);//开始绘制第二层图形,设置画笔颜色为第二层的颜色
            mCanvas.drawPath(mPath1 , mPaint);//绘制第二层图形路径
            mPaint.setColor(xmlWave3Color);//开始绘制第三层图形,设置画笔颜色为第三层的颜色
            mCanvas.drawPath(mPath2 , mPaint);//绘制第三层图形路径


            //判移动移量是否已经超过预备周期的宽度,是的话把偏移量重置为0
            if (offset > cycleWidth) {
                offset = 0;
            }
            if (offset1 > cycleWidth) {
                offset1 = 0;
            }
            if (offset2 > cycleWidth) {
                offset2 = 0;
            }
        }catch (Exception e){
        }finally {
            /*mCanvas != null 是因为在华为4.4上,跳转Activity后,mCanvas会变成空,返回的时候有可能报空指针异常*/
            if(mIsDrawing && mCanvas != null){
                mHolder.unlockCanvasAndPost(mCanvas);//解锁,把绘制的内容提交到屏幕上
            }
        }

    }
}
package com.example.demoapplication; import android.Manifest; import android.content.pm.PackageManager; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.MediaRecorder; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.speech.tts.TextToSpeech; import android.util.Base64; import android.util.Log; import android.view.View; import android.view.animation.Animation; import android.view.animation.ScaleAnimation; import android.widget.Button; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedList; import java.util.Locale; import java.util.Queue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * 主活动类,实现录音、播放、网络通信和TTS功能 */ public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener { // 日志标签 private static final String TAG = "AudioRecorder"; // UI控件 private Button startRecordButton, stopRecordButton; private Button playSoundButton, pauseSoundButton, stopSoundButton, resumeSoundButton, clearSoundsButton; private AudioRecord audioRecord; // 音频配置常量 private static final int SAMPLE_RATE = 16000; private static final int BUFFER_SIZE; // 静态代码块计算缓冲区大小 static { int minBufferSize = AudioRecord.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT ); BUFFER_SIZE = Math.max(minBufferSize, 4096); } // 线程和状态管理 private ScheduledExecutorService scheduler; private AtomicBoolean isRecording = new AtomicBoolean(false); private static final int PERMISSION_REQUEST_CODE = 1; private final ExecutorService executorService = Executors.newCachedThreadPool(); // 网络通信相关变量 private ServerSocket serverSocket; private volatile boolean isServerRunning = true; private volatile Socket clientSocket; private volatile BufferedWriter socketWriter; // TTS和音频播放控制 private TextToSpeech ttsEngine; private boolean isTtsInitialized = false; private AudioTrack audioTrack; // 队列用于存储录音数据 private final Queue<byte[]> recordingQueue = new LinkedList<>(); private final Queue<byte[]> pausedQueue = new LinkedList<>(); private final Queue<byte[]> playbackQueue = new LinkedList<>(); // 原子变量确保线程安全的状态管理 private final AtomicBoolean isPlaying = new AtomicBoolean(false); private final AtomicBoolean isPaused = new AtomicBoolean(false); private volatile boolean isPlaybackThreadActive = false; // 锁对象用于同步访问共享资源 private final Object audioTrackLock = new Object(); private final Object playbackQueueLock = new Object(); private final Object recordingQueueLock = new Object(); // 动画资源 private Animation buttonPressAnim; // 主线程Handler用于更新UI private final Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case 0x11: showAnimatedToast("客户端已连接", Toast.LENGTH_SHORT); break; case 0x12: showAnimatedToast("开始录音", Toast.LENGTH_SHORT); sendJsonPacket("startRecorder", null); playTts("开始录音"); startButtonPulseAnimation(startRecordButton); break; case 0x14: showAnimatedToast("停止录音", Toast.LENGTH_SHORT); sendJsonPacket("stopRecorder", null); playTts("停止录音"); stopButtonPulseAnimation(startRecordButton); break; case 0x16: showAnimatedToast("错误: " + msg.obj, Toast.LENGTH_LONG); break; case 0x17: showAnimatedToast("播放完成", Toast.LENGTH_SHORT); isPlaying.set(false); isPlaybackThreadActive = false; updatePlayButtonsState(); stopPlaybackAnimations(); break; case 0x18: showAnimatedToast("已添加到播放队列", Toast.LENGTH_SHORT); break; case 0x19: updatePlayButtonsState(); break; case 0x20: sendJsonPacket("pauseSound", null); playTts("播放暂停"); pausePlaybackAnimations(); break; case 0x21: sendJsonPacket("stopSound", null); playTts("播放停止"); stopPlaybackAnimations(); break; case 0x22: sendJsonPacket("resumeSound", null); playTts("继续播放"); resumePlaybackAnimations(); break; case 0x23: sendJsonPacket("clearSounds", null); playTts("清空所有录音"); break; case 0x24: String base64Data = (String) msg.obj; try { byte[] decodedData = Base64.decode(base64Data, Base64.DEFAULT); addBase64ToPlaybackQueue(decodedData); } catch (IllegalArgumentException e) { Log.e(TAG, "Base64解码失败", e); sendErrorMessage("无效的Base64数据"); } break; } } }; /** * Activity创建方法 * @param savedInstanceState 保存的状态 */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化按钮脉冲动画 initButtonAnimations(); // 初始化TTS引擎 ttsEngine = new TextToSpeech(this, this); initViews(); setupClickListeners(); checkPermissions(); startServer(30000); startSocketListener(); } /** * 初始化按钮动画 */ private void initButtonAnimations() { // 创建按钮点击动画 buttonPressAnim = new ScaleAnimation( 1.0f, 0.9f, 1.0f, 0.9f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ); buttonPressAnim.setDuration(150); buttonPressAnim.setRepeatCount(0); buttonPressAnim.setFillAfter(false); } /** * 初始化视图组件 */ private void initViews() { // 绑定按钮 startRecordButton = findViewById(R.id.startRecordButton); stopRecordButton = findViewById(R.id.stopRecordButton); playSoundButton = findViewById(R.id.playSoundButton); pauseSoundButton = findViewById(R.id.pauseSoundButton); stopSoundButton = findViewById(R.id.stopSoundButton); resumeSoundButton = findViewById(R.id.resumeSoundButton); clearSoundsButton = findViewById(R.id.clearSoundsButton); // 初始按钮状态设置 stopRecordButton.setEnabled(false); pauseSoundButton.setEnabled(false); stopSoundButton.setEnabled(false); resumeSoundButton.setEnabled(false); } /** * 设置按钮点击监听器 */ private void setupClickListeners() { startRecordButton.setOnClickListener(v -> { animateButtonClick(v); startRecording(); }); stopRecordButton.setOnClickListener(v -> { animateButtonClick(v); stopRecording(); }); playSoundButton.setOnClickListener(v -> { animateButtonClick(v); addToPlaybackQueue(); }); pauseSoundButton.setOnClickListener(v -> { animateButtonClick(v); pausePlayback(); handler.sendEmptyMessage(0x20); }); stopSoundButton.setOnClickListener(v -> { animateButtonClick(v); stopPlayback(); handler.sendEmptyMessage(0x21); }); resumeSoundButton.setOnClickListener(v -> { animateButtonClick(v); if (isPaused.get() && !playbackQueue.isEmpty()) { resumePlayback(); handler.sendEmptyMessage(0x22); } }); clearSoundsButton.setOnClickListener(v -> { animateButtonClick(v); clearAllRecordings(); handler.sendEmptyMessage(0x23); }); } /** * 按钮点击动画 */ private void animateButtonClick(View view) { view.startAnimation(buttonPressAnim); } /** * 开始录音时启动脉冲动画 */ private void startButtonPulseAnimation(Button button) { stopButtonPulseAnimation(button); // 先停止之前的动画 Animation pulseAnim = new ScaleAnimation( 1.0f, 1.1f, 1.0f, 1.1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ); pulseAnim.setDuration(500); pulseAnim.setRepeatCount(Animation.INFINITE); pulseAnim.setRepeatMode(Animation.REVERSE); button.startAnimation(pulseAnim); } /** * 停止录音时停止脉冲动画 */ private void stopButtonPulseAnimation(Button button) { button.clearAnimation(); } /** * 播放暂停时的动画效果 */ private void pausePlaybackAnimations() { stopButtonPulseAnimation(playSoundButton); stopButtonPulseAnimation(resumeSoundButton); } /** * 播放恢复时的动画效果 */ private void resumePlaybackAnimations() { startButtonPulseAnimation(resumeSoundButton); } /** * 播放停止时的动画效果 */ private void stopPlaybackAnimations() { stopButtonPulseAnimation(playSoundButton); stopButtonPulseAnimation(resumeSoundButton); } /** * 显示带缩放动画的Toast */ private void showAnimatedToast(String message, int duration) { Toast toast = Toast.makeText(this, message, duration); View view = toast.getView(); Animation anim = new ScaleAnimation( 0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ); anim.setDuration(300); view.startAnimation(anim); toast.show(); } // ==================== 录音功能实现 ==================== /** * 开始录音 */ private void startRecording() { // 检查录音权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { sendErrorMessage("没有录音权限"); return; } // 如果已经在录音,先释放资源 if (isRecording.get()) { releaseAudioResources(); } try { // 初始化录音器 audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, BUFFER_SIZE ); if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { throw new IllegalStateException("AudioRecord 初始化失败"); } // 开始录音 audioRecord.startRecording(); isRecording.set(true); // 更新UI状态 startRecordButton.setEnabled(false); stopRecordButton.setEnabled(true); updatePlayButtonsState(); // 启动录音数据采集线程 if (scheduler != null && !scheduler.isShutdown()) { scheduler.shutdownNow(); } scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this::captureAudioData, 0, 100, TimeUnit.MILLISECONDS); // 发送开始录音通知 handler.sendEmptyMessage(0x12); } catch (Exception e) { Log.e(TAG, "录音启动失败", e); sendErrorMessage("录音启动失败: " + e.getMessage()); releaseAudioResources(); } } /** * 停止录音 */ private void stopRecording() { if (!isRecording.get()) return; isRecording.set(false); releaseAudioResources(); // 更新UI状态 stopRecordButton.setEnabled(false); startRecordButton.setEnabled(true); updatePlayButtonsState(); // 发送停止录音通知 handler.sendEmptyMessage(0x14); } /** * 采集音频数据并保存到队列 */ private void captureAudioData() { if (!isRecording.get() || audioRecord == null) return; byte[] buffer = new byte[BUFFER_SIZE]; try { int bytesRead = audioRecord.read(buffer, 0, BUFFER_SIZE); if (bytesRead > 0) { // 将录制的音频数据保存到队列 synchronized (recordingQueueLock) { recordingQueue.offer(buffer.clone()); } // 发送录音数据包 String base64Data = Base64.encodeToString(buffer, Base64.DEFAULT); sendJsonPacket("recording", base64Data); } } catch (Exception e) { Log.e(TAG, "音频采集失败", e); } } // ==================== 录音功能结束 ==================== // ==================== 播放功能实现 ==================== /** * 添加当前录音到播放队列 */ private void addToPlaybackQueue() { if (recordingQueue.isEmpty()) { showAnimatedToast("没有可播放的录音", Toast.LENGTH_SHORT); return; } // 创建录音数据副本 Queue<byte[]> recordingCopy = new LinkedList<>(); synchronized (recordingQueueLock) { for (byte[] data : recordingQueue) { recordingCopy.offer(data.clone()); } } // 添加到播放队列 synchronized (playbackQueueLock) { playbackQueue.addAll(recordingCopy); } // 如果当前没有播放,立即开始播放 if (!isPlaybackThreadActive && !isPlaying.get()) { executorService.execute(this::playRecordingQueue); startButtonPulseAnimation(playSoundButton); } else { handler.sendEmptyMessage(0x18); } } /** * 将Base64编码的音频数据添加到播放队列 * @param decodedData 解码后的音频数据 */ private void addBase64ToPlaybackQueue(byte[] decodedData) { if (decodedData == null || decodedData.length == 0) { Log.w(TAG, "无效的音频数据"); return; } ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedData); byte[] buffer; int bytesRead; // 使用固定大小的缓冲区读取数据 buffer = new byte[4096]; try { while ((bytesRead = inputStream.read(buffer)) != -1) { if (bytesRead > 0) { byte[] dataChunk = new byte[bytesRead]; System.arraycopy(buffer, 0, dataChunk, 0, bytesRead); synchronized (playbackQueueLock) { playbackQueue.offer(dataChunk); } } } } catch (IOException e) { Log.e(TAG, "读取音频数据失败", e); } finally { try { inputStream.close(); } catch (IOException e) { Log.e(TAG, "关闭输入流失败", e); } } // 如果当前没有播放,立即开始播放 if (!isPlaybackThreadActive && !isPlaying.get()) { executorService.execute(this::playRecordingQueue); startButtonPulseAnimation(playSoundButton); } else { handler.sendEmptyMessage(0x18); } } /** * 播放录音队列 */ private void playRecordingQueue() { isPlaybackThreadActive = true; isPlaying.set(true); isPaused.set(false); handler.sendEmptyMessage(0x19); // 配置音频播放器 int bufferSize = AudioTrack.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT ); // 创建新的AudioTrack synchronized (audioTrackLock) { if (audioTrack != null) { try { audioTrack.stop(); audioTrack.release(); } catch (Exception e) { Log.e(TAG, "释放AudioTrack失败", e); } } try { audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM ); audioTrack.play(); } catch (IllegalStateException e) { Log.e(TAG, "创建AudioTrack失败", e); stopPlayback(); return; } } // 播放队列中的所有录音数据 while (isPlaying.get() && !playbackQueue.isEmpty()) { if (isPaused.get()) { // 暂停状态,等待恢复 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } continue; } byte[] audioData; synchronized (playbackQueueLock) { audioData = playbackQueue.poll(); } if (audioData != null) { synchronized (audioTrackLock) { if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) { try { audioTrack.write(audioData, 0, audioData.length); } catch (IllegalStateException e) { Log.e(TAG, "音频写入失败: " + e.getMessage()); break; } } else { Log.w(TAG, "AudioTrack不可用,停止播放"); break; } } } } // 确保播放完成时正确释放资源 try { synchronized (audioTrackLock) { if (audioTrack != null) { if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { audioTrack.stop(); } audioTrack.release(); audioTrack = null; } } } catch (Exception e) { Log.e(TAG, "播放完成后释放资源失败", e); } // 播放完成 stopPlayback(); handler.sendEmptyMessage(0x17); } /** * 暂停播放 */ private void pausePlayback() { if (!isPlaying.get() || isPaused.get()) return; isPaused.set(true); // 保存当前播放位置 synchronized (playbackQueueLock) { pausedQueue.clear(); pausedQueue.addAll(playbackQueue); playbackQueue.clear(); } // 暂停音频播放 synchronized (audioTrackLock) { if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { try { audioTrack.pause(); } catch (IllegalStateException e) { Log.e(TAG, "暂停播放失败: " + e.getMessage()); } } } handler.sendEmptyMessage(0x19); runOnUiThread(() -> showAnimatedToast("播放已暂停", Toast.LENGTH_SHORT) ); } /** * 继续播放 */ private void resumePlayback() { if (!isPaused.get() || pausedQueue.isEmpty()) { return; } isPaused.set(false); isPlaying.set(true); // 恢复播放位置 synchronized (playbackQueueLock) { playbackQueue.clear(); playbackQueue.addAll(pausedQueue); pausedQueue.clear(); } // 恢复音频播放 synchronized (audioTrackLock) { if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED) { try { audioTrack.play(); } catch (IllegalStateException e) { Log.e(TAG, "恢复播放失败: " + e.getMessage()); } } } handler.sendEmptyMessage(0x19); runOnUiThread(() -> showAnimatedToast("继续播放", Toast.LENGTH_SHORT) ); } /** * 停止播放 */ private void stopPlayback() { isPlaying.set(false); isPaused.set(false); synchronized (audioTrackLock) { if (audioTrack != null) { try { if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING || audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED) { audioTrack.stop(); } audioTrack.release(); } catch (IllegalStateException e) { Log.e(TAG, "停止播放失败: " + e.getMessage()); } finally { audioTrack = null; } } } synchronized (playbackQueueLock) { playbackQueue.clear(); } pausedQueue.clear(); runOnUiThread(() -> { handler.sendEmptyMessage(0x19); showAnimatedToast("播放已停止", Toast.LENGTH_SHORT); }); } private void clearAllRecordings() { stopPlayback(); synchronized (recordingQueueLock) { recordingQueue.clear(); } pausedQueue.clear(); synchronized (playbackQueueLock) { playbackQueue.clear(); } handler.sendEmptyMessage(0x19); runOnUiThread(() -> showAnimatedToast("所有录音已清除", Toast.LENGTH_SHORT) ); } // ==================== 播放功能结束 ==================== // ==================== 辅助方法 ==================== /** * 更新播放按钮状态 */ private void updatePlayButtonsState() { runOnUiThread(() -> { boolean hasRecordings = !recordingQueue.isEmpty() || !pausedQueue.isEmpty(); boolean isPlayingState = isPlaying.get() && !isPaused.get(); playSoundButton.setEnabled(hasRecordings && !isPlayingState); pauseSoundButton.setEnabled(isPlayingState); stopSoundButton.setEnabled(isPlaying.get() || isPaused.get()); resumeSoundButton.setEnabled(!playbackQueue.isEmpty() && isPaused.get()); clearSoundsButton.setEnabled(hasRecordings); }); } /** * 播放TTS语音 * @param text 要播放的文本 */ private void playTts(String text) { if (isTtsInitialized) { ttsEngine.speak(text, TextToSpeech.QUEUE_FLUSH, null); } } /** * 释放音频资源 */ private void releaseAudioResources() { if (audioRecord != null) { try { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } audioRecord.release(); } catch (IllegalStateException e) { Log.e(TAG, "停止录音失败", e); } finally { audioRecord = null; } } if (scheduler != null) { try { scheduler.shutdownNow(); if (!scheduler.awaitTermination(500, TimeUnit.MILLISECONDS)) { Log.w(TAG, "录音线程池未正常关闭"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { scheduler = null; } } } /** * 发送JSON格式的数据包 * @param type 数据包类型 * @param data 数据内容(可以为null) */ private void sendJsonPacket(String type, Object data) { if (clientSocket == null || clientSocket.isClosed() || socketWriter == null) { return; } try { JSONObject packet = new JSONObject(); packet.put("type", type); if (data != null) { packet.put("data", data); } synchronized (this) { if (socketWriter != null) { socketWriter.write(packet.toString()); socketWriter.write("\n\n"); socketWriter.flush(); } } } catch (Exception e) { Log.e(TAG, "发送数据包失败: " + type, e); } } /** * 发送错误消息 * @param message 错误信息 */ private void sendErrorMessage(String message) { handler.obtainMessage(0x16, message).sendToTarget(); } /** * 检查权限 */ private void checkPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE); } } /** * 启动服务器 * @param port 监听端口 */ private void startServer(int port) { executorService.execute(() -> { try { serverSocket = new ServerSocket(port); Log.i(TAG, "服务器启动: " + port); while (isServerRunning) { try { Socket socket = serverSocket.accept(); clientSocket = socket; synchronized (this) { socketWriter = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); } handler.sendEmptyMessage(0x11); } catch (IOException e) { if (isServerRunning) Log.e(TAG, "接受连接失败", e); } } } catch (IOException e) { Log.e(TAG, "服务器启动失败", e); runOnUiThread(() -> Toast.makeText(this, "服务器启动失败: " + e.getMessage(), Toast.LENGTH_LONG).show()); } finally { closeServerSocket(); } }); } /** * 启动Socket监听 */ private void startSocketListener() { executorService.execute(() -> { while (true) { if (clientSocket != null && !clientSocket.isClosed()) { try { BufferedReader reader = new BufferedReader( new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); StringBuilder packetBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { if (line.isEmpty()) { // 收到两个换行符,表示一个数据包结束 if (packetBuilder.length() > 0) { String packet = packetBuilder.toString(); Log.d(TAG, "收到数据包: " + packet); try { JSONObject command = new JSONObject(packet); String type = command.getString("type"); switch (type) { case "playSound": String base64Data = command.getString("data"); Message msg = handler.obtainMessage(0x24, base64Data); handler.sendMessage(msg); break; case "pauseSound": handler.sendEmptyMessage(0x20); break; case "stopSound": handler.sendEmptyMessage(0x21); break; case "resumeSound": handler.sendEmptyMessage(0x22); break; case "clearSounds": handler.sendEmptyMessage(0x23); break; default: Log.w(TAG, "未知指令类型: " + type); } } catch (JSONException e) { Log.e(TAG, "JSON解析失败", e); } // 重置包构建器 packetBuilder.setLength(0); } } else { // 添加数据到当前包 packetBuilder.append(line); } } } catch (IOException e) { Log.e(TAG, "Socket读取失败", e); } } else { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } }); } /** * 关闭服务器Socket */ private void closeServerSocket() { try { if (serverSocket != null && !serverSocket.isClosed()) { serverSocket.close(); } } catch (IOException e) { Log.w(TAG, "关闭ServerSocket失败", e); } } /** * TTS初始化回调 */ @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { int result = ttsEngine.setLanguage(Locale.CHINESE); if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { Log.e(TAG, "TTS语言不支持中文"); } else { isTtsInitialized = true; } } } /** * 活动销毁时调用 */ @Override protected void onDestroy() { super.onDestroy(); isServerRunning = false; if (ttsEngine != null) { ttsEngine.stop(); ttsEngine.shutdown(); } closeServerSocket(); closeSocket(clientSocket); // 停止所有录音和播放 stopRecording(); stopPlayback(); // 优雅关闭线程池 executorService.shutdown(); try { if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow(); Thread.currentThread().interrupt(); } // 确保所有资源释放 releaseAudioResources(); } /** * 关闭Socket * @param socket 要关闭的Socket */ private void closeSocket(Socket socket) { try { if (socket != null && !socket.isClosed()) { socket.close(); } } catch (IOException e) { Log.w(TAG, "关闭Socket失败", e); } if (socket == clientSocket) { clientSocket = null; synchronized (this) { socketWriter = null; } } } } 在这个页面添加点样式。录音时候带些波浪回馈,美化页面
最新发布
07-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值