Android錄音功能(仿微信)

提要:需求是開發類似微信發語音的功能,沒有語音轉文字。網上看了一些代碼,不能拿來直接用,部分代碼邏輯有問題,所以想把自己的代碼貼出來,僅供參考。

功能:

a、設置最大錄音時長和錄音倒計時(爲了方便測試,最大時長設置爲15秒,開始倒計時設置爲7秒)

b、在錄音之前檢查錄音和存儲權限

源碼:

1、錄音對話框管理類DialogManager:


/**
 * 功能:錄音對話框管理類
 */
public class DialogManager {
    private AlertDialog.Builder builder;
    private AlertDialog dialog;
    private ImageView mIcon;
    private ImageView mVoice;
    private TextView mLabel;

    private Context context;

    /**
     * 構造方法
     *
     * @param context Activity級別的Context
     */
    public DialogManager(Context context) {
        this.context = context;
    }

    /**
     * 顯示錄音的對話框
     */
    public void showRecordingDialog() {
        builder = new AlertDialog.Builder(context, R.style.AudioRecorderDialogStyle);
        LayoutInflater inflater = LayoutInflater.from(context);
        View view = inflater.inflate(R.layout.audio_recorder_dialog, null);
        mIcon = view.findViewById(R.id.iv_dialog_icon);
        mVoice = view.findViewById(R.id.iv_dialog_voice);
        mLabel = view.findViewById(R.id.tv_dialog_label);

        builder.setView(view);
        dialog = builder.create();
        dialog.show();
        dialog.setCanceledOnTouchOutside(false);
    }

    /**
     * 正在播放時的狀態
     */
    public void recording() {
        if (dialog != null && dialog.isShowing()) { //顯示狀態
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.VISIBLE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.ic_audio_recorder);
            mVoice.setImageResource(R.drawable.ic_audio_v1);
            mLabel.setText(R.string.audio_record_dialog_up_to_cancel);
        }
    }

    /**
     * 顯示想取消的對話框
     */
    public void wantToCancel() {
        if (dialog != null && dialog.isShowing()) { //顯示狀態
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.GONE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.ic_audio_cancel);
            mLabel.setText(R.string.audio_record_dialog_release_to_cancel);
        }
    }

    /**
     * 顯示時間過短的對話框
     */
    public void tooShort() {
        if (dialog != null && dialog.isShowing()) { //顯示狀態
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.GONE);
            mLabel.setVisibility(View.VISIBLE);

            mLabel.setText(R.string.audio_record_dialog_too_short);
        }
    }

    // 顯示取消的對話框
    public void dismissDialog() {
        if (dialog != null && dialog.isShowing()) { //顯示狀態
            dialog.dismiss();
            dialog = null;
        }
    }

    /**
     * 顯示更新音量級別的對話框
     *
     * @param level 1-7
     */
    public void updateVoiceLevel(int level) {
        if (dialog != null && dialog.isShowing()) { //顯示狀態
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.VISIBLE);
            mLabel.setVisibility(View.VISIBLE);

            int resId = context.getResources().getIdentifier("ic_audio_v" + level, "drawable", context.getPackageName());
            mVoice.setImageResource(resId);
        }
    }

    public void updateTime(int time) {
        if (dialog != null && dialog.isShowing()) { //顯示狀態
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.VISIBLE);
            mLabel.setVisibility(View.VISIBLE);
            mLabel.setText(time + "s");
        }
    }
}

2、錄音管理類AudioManager


/**
 * 功能:錄音管理類
 */
public class AudioManager {
    private MediaRecorder mMediaRecorder;
    private String mDir;
    private String mCurrentFilePath;

    private static AudioManager mInstance;

    private boolean isPrepared;

    private AudioManager(String dir) {
        this.mDir = dir;
    }

    //單例模式:在這裏實例化AudioManager並傳入錄音文件地址
    public static AudioManager getInstance(String dir) {
        if (mInstance == null) {
            synchronized (AudioManager.class) {
                if (mInstance == null) {
                    mInstance = new AudioManager(dir);
                }
            }
        }
        return mInstance;
    }

    /**
     * 回調準備完畢
     */
    public interface AudioStateListener {
        void wellPrepared();
    }

    public AudioStateListener mListener;

    /**
     * 回調方法
     */
    public void setOnAudioStateListener(AudioStateListener listener) {
        mListener = listener;
    }

    /**
     * 準備
     */
    public void prepareAudio() {
        try {
            isPrepared = false;
            File dir = FileUtils.createNewFile(mDir);
            String fileName = generateFileName();

            File file = new File(dir, fileName);
            mCurrentFilePath = file.getAbsolutePath();
            Logger.t("AudioManager").i("audio file name :" + mCurrentFilePath);

            mMediaRecorder = new MediaRecorder();
            //設置輸出文件
            mMediaRecorder.setOutputFile(mCurrentFilePath);
            //設置MediaRecorder的音頻源爲麥克風
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            //設置音頻格式
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //設置音頻的格式爲AAC
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            //準備錄音
            mMediaRecorder.prepare();
            //開始
            mMediaRecorder.start();
            //準備結束
            isPrepared = true;
            if (mListener != null) {
                mListener.wellPrepared();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 隨機生成文件的名稱
     */
    private String generateFileName() {
        return UUID.randomUUID().toString() + ".m4a";
    }

    public int getVoiceLevel(int maxLevel) {
        if (isPrepared) {
            try {
                //獲得最大的振幅getMaxAmplitude() 1-32767
                return maxLevel * mMediaRecorder.getMaxAmplitude() / 32768 + 1;
            } catch (Exception e) {
            }
        }
        return 1;
    }

    /**
     * 釋放資源
     */
    public void release() {
        if (mMediaRecorder != null) {
            mMediaRecorder.stop();
            mMediaRecorder.release();
            mMediaRecorder = null;
        }
    }

    public void cancel() {
        release();
        if (mCurrentFilePath != null) {
            File file = new File(mCurrentFilePath);
            FileUtils.deleteFile(file);
            mCurrentFilePath = null;
        }
    }

    public String getCurrentFilePath() {
        return mCurrentFilePath;
    }
}

3、自定義錄音按鈕AudioRecorderButton

/**
 * 功能:錄音按鈕
 */
public class AudioRecorderButton extends AppCompatButton {
    private Context mContext;
    //取消錄音Y軸位移
    private static final int DISTANCE_Y_CANCEL = 80;
    //錄音最大時長限制
    private static final int AUDIO_RECORDER_MAX_TIME = 15;
    //錄音倒計時時間
    private static final int AUDIO_RECORDER_COUNT_DOWN = 7;
    //狀態
    private static final int STATE_NORMAL = 1;// 默認的狀態
    private static final int STATE_RECORDING = 2;// 正在錄音
    private static final int STATE_WANT_TO_CANCEL = 3;// 希望取消
    //當前的狀態
    private int mCurrentState = STATE_NORMAL;
    //已經開始錄音
    private boolean isRecording = false;
    //是否觸發onLongClick
    private boolean mReady;

    private DialogManager mDialogManager;
    private AudioManager mAudioManager;
    private android.media.AudioManager audioManager;

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

    public AudioRecorderButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        mDialogManager = new DialogManager(context);
        audioManager = (android.media.AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

        String dir = SdUtils.getCustomFolder("Audios");//創建文件夾
        mAudioManager = AudioManager.getInstance(dir);
        mAudioManager.setOnAudioStateListener(new AudioManager.AudioStateListener() {
            @Override
            public void wellPrepared() {
                mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
            }
        });
        //按鈕長按 準備錄音 包括start
        setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                //先判斷有沒有錄音和存儲權限,有則開始錄音,沒有就申請權限
                int hasAudioPermission = ContextCompat.checkSelfPermission(mContext, Manifest.permission.RECORD_AUDIO);
                int hasStoragePermission = ContextCompat.checkSelfPermission(mContext, Manifest.permission.WRITE_EXTERNAL_STORAGE);
                if (hasAudioPermission == PackageManager.PERMISSION_GRANTED && hasStoragePermission == PackageManager.PERMISSION_GRANTED) {
                    mReady = true;
                    mAudioManager.prepareAudio();
                } else {
                    RxPermissions permissions = new RxPermissions((FragmentActivity) mContext);
                    Disposable disposable = permissions.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                            .subscribe(new Consumer<Boolean>() {
                                @Override
                                public void accept(Boolean granted) {
                                    if (!granted) {
                                        ToastUtils.showShort("發送語音功能需要賦予錄音和存儲權限");
                                    }
                                }
                            });
                }
                return true;
            }
        });
    }

    private static final int MSG_AUDIO_PREPARED = 0X110;
    private static final int MSG_VOICE_CHANGED = 0X111;
    private static final int MSG_DIALOG_DISMISS = 0X112;
    private static final int MSG_TIME_OUT = 0x113;
    private static final int UPDATE_TIME = 0x114;

    private boolean mThreadFlag = false;
    //錄音時長
    private float mTime;

    //獲取音量大小的Runnable
    private Runnable mGetVoiceLevelRunnable = new Runnable() {
        @Override
        public void run() {
            while (isRecording) {
                try {
                    Thread.sleep(100);
                    mTime += 0.1f;
                    mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);
                    if (mTime >= AUDIO_RECORDER_MAX_TIME) {//如果時間超過60秒,自動結束錄音
                        while (!mThreadFlag) {//記錄已經結束了錄音,不需要再次結束,以免出現問題
                            mDialogManager.dismissDialog();
                            mAudioManager.release();
                            if (audioFinishRecorderListener != null) {
                                //先回調,再Reset,不然回調中的時間是0
                                audioFinishRecorderListener.onFinish(mTime, mAudioManager.getCurrentFilePath());
                                mHandler.sendEmptyMessage(MSG_TIME_OUT);
                            }
                            mThreadFlag = !mThreadFlag;
                        }
                        isRecording = false;
                    } else if (mTime >= AUDIO_RECORDER_COUNT_DOWN) {
                        mHandler.sendEmptyMessage(UPDATE_TIME);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_AUDIO_PREPARED:
                    mDialogManager.showRecordingDialog();
                    isRecording = true;
                    new Thread(mGetVoiceLevelRunnable).start();
                    break;
                case MSG_VOICE_CHANGED:
                    mDialogManager.updateVoiceLevel(mAudioManager.getVoiceLevel(7));
                    break;
                case MSG_DIALOG_DISMISS:
                    mDialogManager.dismissDialog();
                    break;
                case MSG_TIME_OUT:
                    reset();
                    break;
                case UPDATE_TIME:
                    int countDown = (int) (AUDIO_RECORDER_MAX_TIME - mTime);
                    mDialogManager.updateTime(countDown);
                    break;
            }
            return true;
        }
    });

    /**
     * 錄音完成後的回調
     */
    public interface AudioFinishRecorderListener {
        /**
         * @param seconds  時長
         * @param filePath 文件
         */
        void onFinish(float seconds, String filePath);
    }

    private AudioFinishRecorderListener audioFinishRecorderListener;

    public void setAudioFinishRecorderListener(AudioFinishRecorderListener listener) {
        audioFinishRecorderListener = listener;
    }

    android.media.AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = new android.media.AudioManager.OnAudioFocusChangeListener() {
        @Override
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == android.media.AudioManager.AUDIOFOCUS_LOSS) {
                audioManager.abandonAudioFocus(onAudioFocusChangeListener);
            }
        }
    };

    public void myRequestAudioFocus() {
        audioManager.requestAudioFocus(onAudioFocusChangeListener, android.media.AudioManager.STREAM_MUSIC, android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Logger.t("AudioManager").i("x :" + event.getX() + "-Y:" + event.getY());
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mThreadFlag = false;
                isRecording = true;
                changeState(STATE_RECORDING);
                myRequestAudioFocus();
                break;
            case MotionEvent.ACTION_MOVE:
                if (isRecording) {
                    //根據想x,y的座標,判斷是否想要取消
                    if (event.getY() < 0 && Math.abs(event.getY()) > DISTANCE_Y_CANCEL) {
                        changeState(STATE_WANT_TO_CANCEL);
                    } else {
                        changeState(STATE_RECORDING);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                //如果longClick 沒觸發
                if (!mReady) {
                    reset();
                    return super.onTouchEvent(event);
                }
                //觸發了onLongClick 沒準備好,但是已經prepared已經start
                //所以消除文件夾
                if (!isRecording || mTime < 1.0f) {
                    mDialogManager.tooShort();
                    mAudioManager.cancel();
                    mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS, 1000);
                } else if (mCurrentState == STATE_RECORDING) {//正常錄製結束
                    mDialogManager.dismissDialog();
                    mAudioManager.release();
                    if (audioFinishRecorderListener != null) {
                        audioFinishRecorderListener.onFinish(mTime, mAudioManager.getCurrentFilePath());
                    }
                } else if (mCurrentState == STATE_WANT_TO_CANCEL) {
                    mDialogManager.dismissDialog();
                    mAudioManager.cancel();
                }
                reset();
                audioManager.abandonAudioFocus(onAudioFocusChangeListener);
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 恢復狀態 標誌位
     */
    private void reset() {
        isRecording = false;
        mTime = 0;
        mReady = false;
        changeState(STATE_NORMAL);
    }

    /**
     * 改變狀態
     */
    private void changeState(int state) {
        if (mCurrentState != state) {
            mCurrentState = state;
            switch (state) {
                case STATE_NORMAL:
                    setText(R.string.audio_record_button_normal);
                    break;
                case STATE_RECORDING:
                    if (isRecording) {
                        mDialogManager.recording();
                    }
                    setText(R.string.audio_record_button_recording);
                    break;
                case STATE_WANT_TO_CANCEL:
                    mDialogManager.wantToCancel();
                    setText(R.string.audio_record_button_cancel);
                    break;
            }
        }
    }
}

4、DialogStyle

<!--App Base Theme-->
<style name="AppThemeParent" parent="Theme.AppCompat.Light.NoActionBar">
    <!--不顯示狀態欄:22之前-->
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowAnimationStyle">@style/ActivityAnimTheme</item><!--Activity動畫-->
    <item name="actionOverflowMenuStyle">@style/MenuStyle</item><!--toolbar菜單樣式-->
</style>

<!--Dialog式的Activity-->
<style name="ActivityDialogStyle" parent="AppThemeParent">
    <item name="android:windowBackground">@android:color/transparent</item>
    <!-- 浮於Activity之上 -->
    <item name="android:windowIsFloating">true</item>
    <!-- 邊框 -->
    <item name="android:windowFrame">@null</item>
    <!-- Dialog以外的區域模糊效果 -->
    <item name="android:backgroundDimEnabled">true</item>
    <!-- 半透明 -->
    <item name="android:windowIsTranslucent">true</item>
    <!-- Dialog進入及退出動畫 -->
    <item name="android:windowAnimationStyle">@style/ActivityDialogAnimation</item>
</style>

<!--Audio Recorder Dialog-->
<style name="AudioRecorderDialogStyle" parent="ActivityDialogStyle">
    <!-- Dialog以外的區域模糊效果 -->
    <item name="android:backgroundDimEnabled">false</item>
</style>

<!-- Dialog動畫:漸入漸出-->
<style name="ActivityDialogAnimation" parent="@android:style/Animation.Dialog">
    <item name="android:windowEnterAnimation">@anim/fade_in</item>
    <item name="android:windowExitAnimation">@anim/fade_out</item>
</style>

5、DialogLayout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/audio_recorder_dialog_bg"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="20dp">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/iv_dialog_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_audio_recorder" />

        <ImageView
            android:id="@+id/iv_dialog_voice"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_audio_v1" />

    </LinearLayout>

    <TextView
        android:id="@+id/tv_dialog_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:text="@string/audio_record_dialog_up_to_cancel"
        android:textColor="@color/white"
        android:textSize="15dp" />
</LinearLayout>

6、用到的字符串

<!--AudioRecord-->
<string name="audio_record_button_normal">按住&#160;說話</string>
<string name="audio_record_button_recording">鬆開&#160;結束</string>
<string name="audio_record_button_cancel">鬆開手指&#160;取消發送</string>
<string name="audio_record_dialog_up_to_cancel">手指上劃,取消發送</string>
<string name="audio_record_dialog_release_to_cancel">鬆開手指,取消發送</string>
<string name="audio_record_dialog_too_short">錄音時間過短</string>

7、使用:按鈕的樣式不需要寫在自定義Button中,方便使用

<com.kidney.base_library.view.audioRecorder.AudioRecorderButton
    android:id="@+id/btn_audio_recorder"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/audio_record_button_normal" />

 AudioRecorderButton audioRecorderButton = findViewById(R.id.btn_audio_recorder);
 audioRecorderButton.setAudioFinishRecorderListener(new AudioRecorderButton.AudioFinishRecorderListener() {
     @Override
     public void onFinish(float seconds, String filePath) {
         Logger.i(seconds + "秒:" + filePath);
     }
 });

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章