美文网首页手机移动程序开发Android开发Android进阶之路
Android自定义控件(神级)+MediaRecoder录音

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

作者: e4e52c116681 | 来源:发表于2019-01-05 17:02 被阅读34次

    零、前言

    总算想到一个神级的自定义控件了
    前方高能预警,萌新自带零食饮料
    本文的前置知识你需简单了解: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

    相关文章

      网友评论

        本文标题:Android自定义控件(神级)+MediaRecoder录音

        本文链接:https://www.haomeiwen.com/subject/jrcdrqtx.html