自定义SurfaceView之音频录制圆形进度条

本篇文章介绍自定义SurfaceView来实现如下的效果

这里写图片描述

由于对于SurfaceView不是很熟练,这次拿它来练手

SurfaceView用途:

一般View可以满足大部分的绘图需求,但如果需要并发执行复杂耗时的逻辑的时候,就会不断阻塞主线程,导致画面卡顿,为了避免这种问题的发生,我们应该使用SurfaceView来解决这个问题

SurfaceView使用介绍可以参考另外一篇博客:Android绘图机制与处理技巧(一)SurfaceView

总共由以下几个部分组成:

  • 按钮按下效果
  • 灰色总进度条
  • 绿色圆形进度条
  • 绿色小圆圈

具体实现过程分析:

创建自定义SurfaceView需要继承自SurfaceView,并实现SurfaceHolder.Callback, Runnable接口

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

然后需要重写三个方法:surfaceCreated(),surfaceChanged(),surfaceDestroyed()和run()

定义的成员变量,具体用途可以看注释

//选择按钮图标
    private boolean isChangeCenterBitmap = true;
    //持续画图
    private boolean isSustainedDraw = false;
    private boolean isStart = true;
    //是否画小圆点,默认为true
    private boolean isDrawSmallCircle = true;
    //小圆点颜色
    private int smallCircleColor;
    //是否画圆弧,默认为true
    private boolean isDrawArc = true;
    //圆弧颜色
    private int arcColor;

    private CompleteTimeCallBack completeTimeCallBack;
    private SurfaceHolder holder = null;
    //绘图属性---------
    private Canvas canvas;

    //录音按钮
    private Paint pPaint;
    private int px;//坐标x位置
    private int py;//坐标y位置
    //radius = defaultRadius * dp
    private int radius;//半径
    //defaultRadius 默认值为40
    private int defaultRadius = 40;
    //起始角度
    private float startAngle = 270;
    //进度
    private float sweepAngle;
    //小球起始角度默认等于进度条起始角度
    private float angle, duration = 20;
    private int startBitmap;
    private int stopBitmap;
    //中心图片的范围,默认为10,值越大图片越小
    private int centerBitmap_margin = 10;
    private int dp;
    private Bitmap bitmap;
    private boolean isGetBitmap = false;
    long a, b, calculateTime, sleepTime = 60, correctSleepTime;

然后我们需要对SurfaceHolder以及一些其他的属性初始化

public void init() {

        //获取mSurfaceHolder
        holder = getHolder();
        holder.addCallback(this);
        //背景设为透明
        if (!isInEditMode()) {
            setZOrderOnTop(true);
        }
        holder.setFormat(PixelFormat.TRANSLUCENT);

        //设置进度条
        pPaint = new Paint();
        pPaint.setAntiAlias(true);
        pPaint.setStrokeWidth(4);
        pPaint.setStyle(Paint.Style.STROKE);
        angle = startAngle;
        sweepAngle = 0;

        dp = Resources.getSystem().getDisplayMetrics().densityDpi / 160;

    }

重写SurfaceView的surfaceCreated()方法

@Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (!isStart) {
            reset();
        }
        radius = defaultRadius * dp;
        isChangeCenterBitmap = true;
        px = this.getWidth() / 2;
        py = this.getHeight() / 2;
        new Thread(this).start();
    }

在这里根据子线程标志位做初始化,以及计算半径,中心点坐标等等,并开启线程

由于我们不用改变SurfaceView大小因此无需在surfaceChanged()方法中写逻辑

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

重写SurfaceView的surfaceDestroyed()方法,在这里将标志位设为false

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

然后重写run()方法,该方法是一个子线程,在这里通过不停循环才进行绘制界面

@Override
    public void run() {
        while (isStart) {
            if (isSustainedDraw) {
                canvas = holder.lockCanvas(); // 获得画布对象,开始对画布画画
                if (canvas == null) {
                    continue;
                }
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                //                canvas.drawColor(canvasColor); // 把画布填充指定颜色
                drawCenterBitmap();
                drawCircle();
                holder.unlockCanvasAndPost(canvas); // 完成画画,把画布显示在屏幕上

                calculateTime = calculateTime + sleepTime;
                try {
                    b = a;
                    a = System.currentTimeMillis();

                    if (b == 0) {
                        correctSleepTime = sleepTime;

                    } else {
                        if ((a - b) >= sleepTime && (a - b) < 2 * sleepTime) {
                            correctSleepTime = sleepTime - (a - b - correctSleepTime);
                        } else if ((a - b) > 2 * sleepTime) {
                            correctSleepTime = 0;
                            //不睡眠
                        } else if ((a - b) < sleepTime) {
                            correctSleepTime = sleepTime - (a - b - correctSleepTime);
                        }
                    }
                    if (correctSleepTime > 0) {
                        Thread.sleep(correctSleepTime);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else if (isChangeCenterBitmap) {
                canvas = holder.lockCanvas(); // 获得画布对象,开始对画布画画
                if (canvas == null) {
                    continue;
                }
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                //                canvas.drawColor(canvasColor); // 把画布填充指定颜色
                drawCenterBitmap();
                drawCircle();
                holder.unlockCanvasAndPost(canvas); // 完成画画,把画布显示在屏幕上
            }
        }
    }

有几个要注意的地方:

  • lockCanvas()用来获取当前canvas绘图对象,绘制完毕后通过holder.unlockCanvasAndPost(canvas)方法来提交画布内容
  • 由于每次循环都是通过lockCanvas()来获取的canvas对象,因此会保留上一次的绘图操作,所以我们每次回之前都需要通过drawcolor()来进行清屏操作
  • isSustainedDraw标记位来判断手指持续按住屏幕的状态,在按下的状态下才会刷新
  • isChangeCenterBitmap标记位用来在手指不触碰屏幕的情况下限制屏幕只刷新一次,节省资源
  • 使用Thread.sleep(correctSleepTime)尽可能的节省系统资源

画按钮图形的方法

private void drawCenterBitmap() {
        int area = radius - centerBitmap_margin * dp;
        RectF imageRect = new RectF(px - area, py - area, px + area, py + area);

        if (isChangeCenterBitmap) {
            if (!isGetBitmap) {
                if (isSustainedDraw) {
                    bitmap = BitmapFactory.decodeResource(getResources(), stopBitmap);
                } else {
                    bitmap = BitmapFactory.decodeResource(getResources(), startBitmap);
                }
            }
            isChangeCenterBitmap = false;
        }
        canvas.drawBitmap(bitmap, null, imageRect, pPaint);
    }

通过isSustainedDraw判断后使用canvas.drawBitmap()方法绘制相应的图片

通过这个方法绘制灰色大圆、绿色圆点和圆弧

public void drawCircle() {

        //绘制大圆
        pPaint.setColor(Color.LTGRAY);
        pPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(px, py, radius, pPaint);

        //绘制原点
        if (isDrawSmallCircle) {
            pPaint.setColor(smallCircleColor);
            pPaint.setStyle(Paint.Style.FILL);

            //radians=angle * Math.PI / 180 角度转弧度公式
            //Math.cos(radians) * defaultRadius cos计算x轴偏移量,sin计算y轴偏移量
            float ballX = (float) (px + radius * Math.cos(angle * Math.PI / 180));
            float ballY = (float) (py + radius * Math.sin(angle * Math.PI / 180));
            canvas.drawCircle(ballX, ballY, 4 * dp, pPaint);
        }

        //绘制圆弧
        if (isDrawArc) {
            pPaint.setStyle(Paint.Style.STROKE);
            //            pPaint.setColor(Color.parseColor(arcColor));
            pPaint.setColor(arcColor);
            RectF rect = new RectF(px - radius, py - radius, px + radius, py + radius);
            canvas.drawArc(rect, startAngle, sweepAngle, false, pPaint);//画弧形
        }

        float speed = 360 / (duration * (1000 / sleepTime));
        //        Log.i(TAG, "drawCircle: speed:"+speed);
        angle = angle + speed;
        if (angle > 360) {
            angle = 0;
        }
        sweepAngle = sweepAngle + speed;
        if (sweepAngle > 360) {
            reset();
            if (completeTimeCallBack != null) {
                completeTimeCallBack.stop();
            }
            isSustainedDraw = false;
            isChangeCenterBitmap = true;
        }
    }

画大圆较简单,设置好paint属性后使用drawCircle()方法即可
画圆弧也比较容易,设置好RectF类的坐标和paint属性后,使用drawArc()方法即可
需要注意的是画圆点,重点在于怎么计算圆点围绕中心旋转时的坐标:
我们可以使用Math.cos(radians) * radius来计算X轴偏移量,Math.sin(radians) * radius来计算Y轴偏移量,radians表示弧度,可以通过angle * Math.PI / 180来计算

用到的包装方法,可以设置一些属性

 public void startDraw() {
        isChangeCenterBitmap = true;
        isSustainedDraw = true;
    }

    public void stopDraw() {
        isChangeCenterBitmap = true;
        isSustainedDraw = false;
    }

    public void reset() {
        isStart = true;
        angle = startAngle;
        sweepAngle = 0;
    }

    //设置圆弧颜色,用#RRGGBB 或者 #AARRGGBB
    public void setArcColor(int arcColor) {
        this.arcColor = arcColor;
    }

    //设置小圆点颜色,用#RRGGBB 或者 #AARRGGBB
    public void setSmallCircleColor(int smallCircleColor) {
        this.smallCircleColor = smallCircleColor;
    }

    public void setDefaultRadius(int defaultRadius) {
        this.defaultRadius = defaultRadius;
    }

    public void setStartBitmap(int startBitmap) {
        this.startBitmap = startBitmap;
    }

    public void setStopBitmap(int stopBitmap) {
        this.stopBitmap = stopBitmap;
    }

    public void setDuration(float duration) {
        this.duration = duration;
    }

xml布局

<com.gavinandre.customviewsamples.view.CircleRecordSurfaceView
        android:id="@+id/circle_record_view"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:layout_centerInParent="true"/>

使用方法

mCircleRecordView.setDuration(6);
        mCircleRecordView.setStartBitmap(R.mipmap.audio_record_mic_btn);
        mCircleRecordView.setStopBitmap(R.mipmap.audio_record_mic_btn_press);
        mCircleRecordView.setArcColor(ContextCompat.getColor(this, R.color.record_green));
        mCircleRecordView.setSmallCircleColor(ContextCompat.getColor(this, R.color.record_green));
        mCircleRecordView.setDefaultRadius(50);
        mCircleRecordView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mCircleRecordView.startDraw();
                        break;
                    case MotionEvent.ACTION_UP:
                        mCircleRecordView.reset();
                        mCircleRecordView.stopDraw();
                        break;
                    default:
                        break;
                }
                return true;
            }
        });

如果要写其他逻辑的话在onTouch方法里添加即可

代码示例:
CustomViewSamples

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值