音频采样UI效果

2,252 阅读4分钟

源码地址

先看一下效果图(这个效果图是以前的,后面更新了代码,画面更加流程,完全没有卡顿):

record.gif

一开始我看了这个效果图,是一脸的懵逼,完全没有思路。按照sdk提供的控件肯定是做不出来这种效果图,只能自己画,通过继承View,绘制UI。

先说一下整体思路

  • 首先这个效果图,需要拆分成两部分,一部分是上面表示录音时间的刻度尺,还有一部分是下面的录音的采样波形图
  • 整个画面的移动,这部分也是最难的一部分。我通过录音时间,和采样频率,计算出刻度尺上滑动的距离,然后通过scrollTo方法移动整个画布,来达到效果的
  • 绘制的过程中,只能绘制当前屏幕的内容,绘制整个刻度尺范围内的采样图的肯定会卡顿

整体的思路基本是这样子。下面讲解一下具体的实现过程。

  • 先定义一个BaseAudioRecord继承自View,该类主要控制滑动,初始化自定义参数等
  • 创建一个AudioRecord 类继承BaseAudioRecord, 该类主要负责画面绘制工作

AudioRecord绘制

1.先讲解一下AudioRecord绘制的第一步,绘制上面体现录音时间的刻度尺的绘制。重写onDraw方法,通过canvas来绘制。

代码如下:

 private void drawScale(Canvas canvas) {
        int firstPoint = (getScrollX() - mDrawOffset) / scaleIntervalLength;
        int lastPoint = (getScrollX() + canvas.getWidth() + mDrawOffset) / (scaleIntervalLength);
        for (int i = firstPoint; i < lastPoint; i++) {
            float locationX = i * scaleIntervalLength;
            if (i % intervalCount == 0) {
                canvas.drawLine(locationX, ruleHorizontalLineHeight - bigScaleStrokeLength, locationX, ruleHorizontalLineHeight, bigScalePaint);
                if (showRuleText) {
                    int index = i / intervalCount;
                    canvas.drawText(formatTime(index), locationX + bigScaleStrokeWidth + 5, ruleHorizontalLineHeight - bigScaleStrokeLength + ruleTextSize / 1.5f, ruleTextPaint);
                }
            } else {
                canvas.drawLine(locationX, ruleHorizontalLineHeight - smallScaleStrokeLength, locationX, ruleHorizontalLineHeight, smallScalePaint);
            }
        }
        //画轮廓线
        canvas.drawLine(getScrollX(), ruleHorizontalLineHeight, getScrollX() + canvas.getWidth(), ruleHorizontalLineHeight, ruleHorizontalLinePaint);
    }

上面代码说明如下:

  • getScrollX() 表示画布移动的距离,往右侧移动是正数,往左侧移动是负数,值表示画布在屏幕内移动的平素点的个数
  • firstPoint 表示绘制的第一个点,减去一个 mDrawOffset 表示往左侧屏幕多绘制了一个缓冲区域
  • lastPoint 表示绘制的最后一个点,加上一个 mDrawOffset表示往右侧屏幕多绘制了一个缓冲区域
  • scaleIntervalLength 表示刻度间隔
  • intervalCount 表示两个大刻度之间小刻度的间隔数

2.绘制中间的采样波形图

代码如下:

private void drawLine(Canvas canvas) {
        int middleLineY = canvas.getHeight() / 2;
        canvas.drawLine(getScrollX(), middleLineY, getScrollX() + canvas.getWidth(), middleLineY, middleHorizontalLinePaint);

        //从数据源中找出需要绘制的矩形
        List<SampleLineModel> drawRectList = getDrawSampleLineList(canvas);
        if (drawRectList == null || drawRectList.size() == 0) {
            return;
        }
        //绘制采样点
        for (SampleLineModel sampleLineModel : drawRectList) {
            canvas.drawLine(sampleLineModel.startX, sampleLineModel.startY, sampleLineModel.stopX, sampleLineModel.stopY, linePaint);
            int invertedStartY = canvas.getHeight() / 2;
            float invertedStopY = invertedStartY + sampleLineModel.stopY - sampleLineModel.startY;
            canvas.drawLine(sampleLineModel.startX, invertedStartY, sampleLineModel.stopX, invertedStopY, lineInvertedPaint);
        }
    }
  • 矩形的绘制,是用drawLine来表示的,矩形宽用线宽表示即可。从中心的水平横线上下各绘制了同等长度的线,用来表示采样的波形图
  • 同样采样波形的绘制,也只绘制屏幕内的采样波形

AudioRecord两块区域绘制的核心代码基本就是这样子了

BaseAudioRecord 控制移动

1.重写onTouchEvent()方法,根据手势移动,来移动画布

核心代码如下:

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        float currentX = event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = currentX;
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = mLastX - currentX;
                mLastX = currentX;
                scrollBy((int) (moveX), 0);
                break;
            }
            
         return true;
    }
  • return true; 将onTouchEvent触摸事件消费掉,一遍能够执行到ACTION_MOVE
  • 通过scrollBy来移动画布,距离通过,手指滑动的距离获取float moveX = mLastX - currentX;

2.通过ObjectAnimator.ofFloat()方法来移动画布,开启动画

核心代码如下: 动画开始:

            float startX = getScrollX();
            //小于半屏的时候,要重新计算偏移量,因为有个左滑的动作
            float endX = maxLength - getMeasuredWidth() / 2;
            float dx = Math.abs(endX - startX);
            final double duration = 1000 * dx / (recordSamplingFrequency * (lineWidth + rectGap));
            animator = ObjectAnimator.ofFloat(this, "translateX", startX, endX);
            animator.setInterpolator(new LinearInterpolator());
            animator.setDuration((long) Math.floor(duration));
            animator.removeAllListeners();
            animator.start();

移动画布:

        public void setTranslateX(float translateX) {
        this.translateX = translateX;
        scrollTo((int) translateX, 0);
        if (isStartRecordTranslateCanvas) {
            translateVerticalLineX = getScrollX() + getMeasuredWidth() / 2 + rectGap;
        }
        onTick(getScrollX() + getMeasuredWidth() / 2);

    }

根据画布移动的距离,算出时间,再根据定义好的采样频率,回调采样函数,生成波形图:

 private void onTick(float translateX) {
   if (isRecording) {
        long duration = (long) (translateX * recordTimeInMillis / maxLength);
        if (duration > getSampleCount() * recordDelayMillis) {
            makeSampleLine(recordCallBack.getSamplePercent());
        }
       
   }
 }

这个自定义的录音采集声音波形的UI基本上就完成了,有兴趣的小伙伴可以去查看源码,有什么不对的地方,欢迎指正交流。

源码中,有一个播放器的类AudioRecordMp3.java,采用了AudioRecord录制的音频,使用了Lame将AudioRecord录制的pcm格式的音频实时转码成MP3格式,支持暂停录制,删除上一段录音的功能。

源码里面还有一个播放声音的波形图,原理和上面类似,效果如下: