Android自定義控制元件(神級)+MediaRecoder錄音

張風捷特烈發表於2019-01-05

零、前言

總算想到一個神級的自定義控制元件了
前方高能預警,萌新自帶零食飲料
本文的前置知識你需簡單瞭解: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

相關文章