Android自定义控件(神级)+MediaRecoder录音

11,355 阅读8分钟

零、前言

总算想到一个神级的自定义控件了
前方高能预警,萌新自带零食饮料
本文的前置知识你需简单了解:Android绘制函数图象及正弦函数的介绍
没错,今天玩自定义控件,和函数、录音有什么关系?用脚趾头稍微想一下就知道了...


废话不多说,看待仿效果:

别激动...这只是待仿的效果(OPPOR15X录音自带),至于能仿成什么样我心里也没底

效果.gif


二、正式开战

1.截两张图分析一下
[1]--上下镜像有没有,做一条,另一条镜像一下就行了  
[2]--颜色渐变色,Paint支持颜色渐变
[3]--一条深,一条浅,就拿深的开刀吧
[4]--两端线较细,这个得琢磨一下 
[5]--算上驻点两条线一共有5个交点,一共两个周期

分析图.png


2.正弦函数的绘制

先别看别的,先画一个正弦函数再说
那一篇用点拼的,现在想想可以用path,这篇用path来画

2.1--分析
A:振幅---默认200
φ:初相,默认0
曲线总长:测试阶段:-600~600 共1200
T:周期--600
ω:2π/T

2.2--绘制正弦函数

正弦函数.png

/**
 * 作者:张风捷特烈<br/>
 * 时间:2018/11/16 0016:9:04<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:旋律视图
 */
public class RhythmView2 extends View {
    private Point mCoo = new Point(800, 500);//原点坐标
    private double mMaxHeight = 200;//最到点
    private double min = -600;//最小x
    private double max = 600;//最大x
    private double φ = 0;//初相
    private double A = mMaxHeight;//振幅
    private double ω;//角频率
    private Paint mPaint;//主画笔
    private Path mPath;//主路径
    private Path mReflexPath;//镜像路径
    
    public RhythmView2(Context context) {
        this(context, null);
    }

    public RhythmView2(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();//初始化
    }

    private void init() {
        //初始化主画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(6);
        //初始化主路径
        mPath = new Path();
        mReflexPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPath.reset();
        mReflexPath.reset();
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(mCoo.x, mCoo.y);
        formPath();
        canvas.drawPath(mPath, mPaint);
        canvas.restore();
    }

    /**
     * 对应法则
     *
     * @param x 原像(自变量)
     * @return 像(因变量)
     */
    private double f(double x) {
        double len = max - min;
        ω = 2 * Math.PI / (rad(len) / 2);
        double y =  A * Math.sin(ω * rad(x) - φ);
        return y;
    }

    private void formPath() {
        mPath.moveTo((float) min, (float) f(min));
        for (double x = min; x <= max; x++) {
            double y = f(x);
            mPath.lineTo((float) x, (float) y);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mAnimator.start();
                break;
        }
        return true;
    }

    private double rad(double deg) {
        return deg / 180 * Math.PI;
    }
}

3.正弦函数的动起来

什么影响正弦函数的横向位移--相位:φ
那还等什么,ValueAnimator走起,从0~2 * Math.PI

横向位移.gif

//数字时间流
mAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
mAnimator.setDuration(1000);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(a -> {
    φ = (float) a.getAnimatedValue();
    invalidate();
});

就这么简单?---是的


4.让凸起的部分渐渐平息

不就是对A值进行渐变嘛...非常简单

减息.gif

mAnimator.addUpdateListener(a -> {
    φ = (float) a.getAnimatedValue();
    A = (float) (mMaxHeight* (1 - (float) a.getAnimatedValue() / (2 * Math.PI)));
    invalidate();
});

二、加入衰减函数与渐变色

1.加入衰减函数

虽然有那么点感觉,但是还是差很多,关键在对应法则,说起来也简单
但是操作起来挺费劲,衰减函数凑了好一会...

加入衰减函数.gif

/**
 * 对应法则
 *
 * @param x 原像(自变量)
 * @return 像(因变量)
 */
private double f(double x) {
    double len = max - min;
    double a = 4 / (4 + Math.pow(rad(x / Math.PI * 800 / len), 4));
    double aa = Math.pow(a, 2.5);
    ω = 2 * Math.PI / (rad(len) / 2);
    double y = aa * A * Math.sin(ω * rad(x) - φ);
    return y;
}

2.加渐变色

什么颜色好呢,好吧,我计较懒,搭条彩虹吧(以前实现过)

加颜色渐变.gif

int[] colors = new int[]{
        Color.parseColor("#F60C0C"),//红
        Color.parseColor("#F3B913"),//橙
        Color.parseColor("#E7F716"),//黄
        Color.parseColor("#3DF30B"),//绿
        Color.parseColor("#0DF6EF"),//青
        Color.parseColor("#0829FB"),//蓝
        Color.parseColor("#B709F4"),//紫
};
float[] pos = new float[]{
        1.f / 7, 2.f / 7, 3.f / 7, 4.f / 7, 5.f / 7, 6.f / 7, 1
};
mPaint.setShader(
        new LinearGradient(
                (int) min, 0, (int) max, 0,
                colors, pos,
                Shader.TileMode.CLAMP
        ));

三、第二条曲线的绘制

两条曲线.gif

1.路径的形成

会了一个,另一个Y镜像一下就行了(y坐标边-y)

private void formPath() {
    mPath.moveTo((float) min, (float) f(min));
    mReflexPath.moveTo((float) min, (float) f(min));
    for (double x = min; x <= max; x++) {
        double y = f(x);
        mPath.lineTo((float) x, (float) y);
        mReflexPath.lineTo((float) x, -(float) y);
    }
}

2.绘制第二条曲线:onDraw

第二条淡一点

mPaint.setAlpha(255);
canvas.drawPath(mPath, mPaint);
mPaint.setAlpha(66);
canvas.drawPath(mReflexPath, mPaint);

3.高度设置

我的用意是在录音是监听音量大小,然后让图象波动
暴漏设置高度的方法,在设置时执行动画,下面是点击设置随机高度效果

设置高度.gif

/**
 * 设置高度
 * @param maxHeight
 */
public void setMaxHeight(double maxHeight) {
    mMaxHeight = maxHeight;
    mAnimator.start();
    invalidate();
}

四、扫尾--封装

该dp的dp,该删的删,该封装的封装,该优化的优化,直接贴代码

/**
 * 作者:张风捷特烈<br/>
 * 时间:2018/11/16 0016:9:04<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:贝塞尔三次曲线--旋律视图
 */
public class RhythmView extends View {
    private double mMaxHeight = 0;//最到点
    private double mPerHeight = 0;//最到点

    private double min;//最小x
    private double max;//最大x

    private double φ = 0;//初相
    private double A = mMaxHeight;//振幅
    private double ω;//角频率

    private Paint mPaint;//主画笔
    private Path mPath;//主路径
    private Path mReflexPath;//镜像路径
    private ValueAnimator mAnimator;
    private int mHeight;
    private int mWidth;

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

    public RhythmView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();//初始化
    }

    private void init() {
        //初始化主画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(dp(2));
        //初始化主路径
        mPath = new Path();
        mReflexPath = new Path();
        //数字时间流
        mAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
        mAnimator.setDuration(1000);
        mAnimator.setRepeatMode(ValueAnimator.RESTART);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.addUpdateListener(a -> {
            φ = (float) a.getAnimatedValue();
            A = (float) (mMaxHeight * mPerHeight * (1 - (float) a.getAnimatedValue() / (2 * Math.PI)));
            invalidate();
        });
    }

    public void setPerHeight(double perHeight) {
        mPerHeight = perHeight;
        mAnimator.start();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        mMaxHeight = mHeight / 2 * 0.9;
        min = -mWidth / 2;
        max = mWidth / 2;
        handleColor();
        setMeasuredDimension(mWidth, mHeight);
    }


    private void handleColor() {
        int[] colors = new int[]{
                Color.parseColor("#33F60C0C"),//红
                Color.parseColor("#F3B913"),//橙
                Color.parseColor("#E7F716"),//黄
                Color.parseColor("#3DF30B"),//绿
                Color.parseColor("#0DF6EF"),//青
                Color.parseColor("#0829FB"),//蓝
                Color.parseColor("#33B709F4"),//紫
        };

        float[] pos = new float[]{
                1.f / 10, 2.f / 7, 3.f / 7, 4.f / 7, 5.f / 7, 9.f / 10, 1
        };

        mPaint.setShader(
                new LinearGradient(
                        (int) min, 0, (int) max, 0,
                        colors, pos,
                        Shader.TileMode.CLAMP
                ));
    }


    @Override
    protected void onDraw(Canvas canvas) {
        mPath.reset();
        mReflexPath.reset();
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(mWidth / 2, mHeight / 2);
        formPath();
        mPaint.setAlpha(255);
        canvas.drawPath(mPath, mPaint);
        mPaint.setAlpha(66);
        canvas.drawPath(mReflexPath, mPaint);
        canvas.restore();
    }

    /**
     * 对应法则
     *
     * @param x 原像(自变量)
     * @return 像(因变量)
     */
    private double f(double x) {
        double len = max - min;
        double a = 4 / (4 + Math.pow(rad(x / Math.PI * 800 / len), 4));
        double aa = Math.pow(a, 2.5);
        ω = 2 * Math.PI / (rad(len) / 2);
        double y = aa * A * Math.sin(ω * rad(x) - φ);
        return y;
    }

    private void formPath() {
        mPath.moveTo((float) min, (float) f(min));
        mReflexPath.moveTo((float) min, (float) f(min));
        for (double x = min; x <= max; x++) {
            double y = f(x);
            mPath.lineTo((float) x, (float) y);
            mReflexPath.lineTo((float) x, -(float) y);
        }

    }

    private double rad(double deg) {
        return deg / 180 * Math.PI;
    }

    protected float dp(float dp) {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

}

五、MediaRecode实现录音

第一天用AudioTrack实现了录音,MediaRecode可以录音也可以录视频
两者的区别AudioTrack麻烦一点,需要自己去操作字节流,但可以精致操作
MediaRecode相当于给你封装好了,你一步步走,给个文件就行了

效果.png


1.录音的辅助类
/**
 * 作者:张风捷特烈
 * 时间:2018/4/16:10:33
 * 邮箱:1981462002@qq.com
 * 说明:MediaRecorder录音帮助类
 */
public class MediaRecorderTask {
    private MediaRecorder mRecorder;
    private long mStartTime;//开始的时间
    private int mAllTime;//总共耗时
    private boolean isRecording;//是否正在录音
    private File mFile;//文件

    private Timer mTimer;
    private final Handler mHandler;

    public MediaRecorderTask() {
        mTimer = new Timer();//创建Timer
        mHandler = new Handler();//创建Handler
    }

    /**
     * 开始录音
     */
    public void start(File file) {
        mAllTime = 0;
        mFile = file;
        if (mRecorder == null) {
            // [1]获取MediaRecorder类的实例
            mRecorder = new MediaRecorder();
        }
        //配置MediaRecorder
        // [2]设置音频的来源
        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        // [3]设置音频的输出格式
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        // [4]采样频率
        mRecorder.setAudioSamplingRate(44100);
        // [5]设置音频的编码方式
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        //[6]音质编码频率:96Kbps
        mRecorder.setAudioEncodingBitRate(96000);
        //[7]设置录音文件位置
        mRecorder.setOutputFile(file.getAbsolutePath());
        try {
            mRecorder.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }
        mStartTime = System.currentTimeMillis();
        if (mRecorder != null) {
            mRecorder.start();
            isRecording = true;

            cbkVolume();
        }
    }

    /**
     * 每隔1秒回调一次音量
     */
    private void cbkVolume() {
        mTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (isRecording) {
                    float per;
                    try {
                        //获取音量大小
                        per = mRecorder.getMaxAmplitude() / 32767f;//最大32767
                    } catch (IllegalStateException e) {
                        e.printStackTrace();
                        per = (float) Math.random();
                    }
                    if (mOnVolumeChangeListener != null) {
                        float finalPer = per;
                        mHandler.post(() -> {
                            mOnVolumeChangeListener.volumeChange(finalPer);
                        });
                    }
                }
            }
        }, 0, 1000);
    }


    public void pause() {
        mAllTime += System.currentTimeMillis() - mStartTime;
        mRecorder.pause(); // [7]暂停录
        isRecording = false;
        mStartTime = System.currentTimeMillis();

    }

    public void resume() {
        mRecorder.resume(); // [8]恢复录
        isRecording = true;

    }

    /**
     * 停止录音
     */
    public void stop() {
        try {
            mAllTime += System.currentTimeMillis() - mStartTime;
            mRecorder.stop(); // [7]停止录
            isRecording = false;
            mRecorder.release();
            mRecorder = null;
        } catch (RuntimeException e) {
            mRecorder.reset();//[8] You can reuse the object by going back
            mRecorder.release(); //[9] Now the object cannot be reused
            mRecorder = null;
            isRecording = false;
            if (mFile.exists())
                mFile.delete();
        }
    }

    public int getAllTime() {
        return mAllTime / 1000;
    }

    //---------设置音量改变监听-------------
    public interface OnVolumeChangeListener {
        void volumeChange(float per);
    }

    private OnVolumeChangeListener mOnVolumeChangeListener;

    public void setOnVolumeChangeListener(OnVolumeChangeListener onVolumeChangeListener) {
        mOnVolumeChangeListener = onVolumeChangeListener;
    }
}

2.使用--Activity中

基本套路和第一篇的录音一致,下面只给出核心的步骤
不明白参见第一篇或源码

//初始化MediaRecorderTask
mMediaRecorderTask = new MediaRecorderTask();

//设置监听---效果的核心
mMediaRecorderTask.setOnVolumeChangeListener(per -> {
    mIdRth.setPerHeight(per);
});

/**
 * 开启录音
 */
private void startRecord() {
    //创建录音文件---这里创建文件不是重点,我直接用了
    mFile = FileHelper.get().createFile("MediaRecorder录音/" + StrUtil.getCurrentTime_yyyyMMddHHmmss() + ".m4a");
    mMediaRecorderTask.start(mFile);
}

/**
 * 停止录制
 */
private void stopRecode() {
    mMediaRecorderTask.stop();
    mIdTvState.setText("录制" + mMediaRecorderTask.getAllTime() + "秒");
}

音频软件打开.png


3.播放音频

昨天已经实现了MediaPlayer播放音频,不废话了,直接拿来用

mMusicPlayer = new MusicPlayer();

mMusicPlayer.start("/sdcard/MediaRecorder录音/20190104195319.m4a");

关于音频的编码,压缩,格式这三者感觉挺烦人的,下一篇把它们捋一下
再玩一下音频的变速和变声操作,今天就到这里


后记:捷文规范

1.本文成长记录及勘误表
项目源码 日期 备注
V0.1-github 2018-1-5 Android自定义控件(神级)+MediaRecode录音
2.更多关于我
笔名 QQ 微信 爱好
张风捷特烈 1981462002 zdl1994328 语言
我的github 我的简书 我的掘金 个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


icon_wx_200.png