1. 程式人生 > >Android實現傳送語音,可限制語音時間,對使用者手勢判斷

Android實現傳送語音,可限制語音時間,對使用者手勢判斷

最近公司專案中需要用到聊天功能,在通過對比網易雲信和環信之後呢還是選擇了使用環信。雖然我很喜歡網易的雲音樂,但環信使用起來確實比較簡單,就選擇了它,今天這篇文章主要是講的關於語音聊天的功能的實現。先來看一下我做的效果圖吧~


由於為了錄製這個動態圖,我用的是模擬器,所以在錄製過程中的麥克風中間的波形沒有波動,但是使用真機的話麥克風的波形是可以隨著說話的音量波動起來的。在使用者鬆開按鈕後將錄音檔案儲存在本地,沒做其他操作,如果你也和我一樣正在做這一塊的話可以嘗試去將這個功能去完善起來。

首先呢來整理一下思路,使用者在按住按鈕之後會進行呼叫麥克風進行錄音,鬆開按鈕後將語音檔案儲存在本地。雖然看起來比較簡單,但是為了實現這個功能,我也參考了其他大神的方法,也花了不少時間,覺得寫的不錯的話,就點個讚唄,嘿嘿嘿。~

閒話不多說,現在來看看我是如何實現的吧。

一共寫了三個類:


首先自定義了一個AudioRecorderButton繼承Button,重寫了onTouch事件:

/**
 * 控制錄音Button
 * 1、重寫onTouchEvent;(changeState方法、wantToCancel方法、reset方法);
 * 2、編寫AudioDialogManage、並與該類AudioRecorderButton進行整合;
 * 3、編寫AudioManage、並與該類AudioRecorderButton進行整合;
 */

public class AudioRecorderButton extends Button implements AudioManage.AudioStateListener {

    /**
     * AudioRecorderButton的三個狀態
     */
    private static final int STATE_NORMAL = 1;           //預設狀態
    private static final int STATE_RECORDERING = 2;      //錄音狀態
    private static final int STATE_WANT_TO_CALCEL = 3;   //取消狀態

    private int mCurState = STATE_NORMAL;    // 當前錄音狀態
    private boolean isRecordering = false;   // 是否已經開始錄音
    private boolean mReady;    // 是否觸發onLongClick

    private static final int DISTANCE_Y_CANCEL = 50;

    private AudioDialogManage audioDialogManage;

    private AudioManage mAudioManage;

    /**
     * 正常錄音完成後的回撥
     */
    public interface AudioFinishRecorderListener{
        void onFinish(int seconds, String FilePath);
    }

    private AudioFinishRecorderListener mListener;

    public void setAudioFinishRecorderListener(AudioFinishRecorderListener listener){
        this.mListener=listener;
    }

    //構造方法
    public AudioRecorderButton(Context context) {
        super(context, null);
        // TODO Auto-generated constructor stub
    }
    public AudioRecorderButton(final Context context, AttributeSet attrs) {
        super(context, attrs);

        audioDialogManage = new AudioDialogManage(getContext());

        String dir = Environment.getExternalStorageDirectory()
                + "/kairui/VoiceCache";                             // 此處需要判斷是否有儲存卡(外存)
        mAudioManage = AudioManage.getInstance(dir);
        mAudioManage.setOnAudioStateListener(this);

        setOnLongClickListener(new OnLongClickListener() {

            @Override
            public boolean onLongClick(View v) {
                mReady = true;
                // 真正顯示應該在audio end prepared以後
                mAudioManage.prepareAudio();
                //return true;
                return false;
            }
        });

        mAudioManage.setOnAudioStatusUpdateListener(new AudioManage.OnAudioStatusUpdateListener() {

            //錄音中....db為聲音分貝,time為錄音時長
            @Override
            public void onUpdate(double db, long time) {
                //根據分貝值來設定錄音時話筒圖示的上下波動
                audioDialogManage.mIcon.getDrawable().setLevel((int) (3000 + 6000 * db / 100));
            }

        });


        // TODO Auto-generated constructor stub
    }

    /*
     * 複寫onTouchEvent
     * @see android.widget.TextView#onTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int action = event.getAction();   //獲取當前Action
        int x = (int) event.getX();       //獲取當前的座標
        int y = (int) event.getY();

        switch (action) {

            case MotionEvent.ACTION_DOWN:
                changeState(STATE_RECORDERING);
                break;

            case MotionEvent.ACTION_MOVE:

                // 已經開始錄音狀態時,根據X、Y的座標,判斷是否想要取消
                if (isRecordering) {
                    if (wantToCancel(x, y)) {
                        changeState(STATE_WANT_TO_CALCEL);
                    } else {
                        changeState(STATE_RECORDERING);
                    }
                }
                break;

            case MotionEvent.ACTION_UP:
                if (!mReady) {   //沒有觸發onLongClick
                    reset();
                    return super.onTouchEvent(event);
                }

                if (!isRecordering || mTime < 900) {  //錄音時間過短
                    audioDialogManage.tooShort();
                    mAudioManage.cancel();
                    mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS, 1300);// 延遲,1.3秒以後關閉“時間過短對話方塊”
                }

                else if (mCurState == STATE_RECORDERING) { //正常錄製結束
                    audioDialogManage.dismissDialog();
                    // release
                    mAudioManage.release();
                    // callbackToAct
                    // 正常錄製結束,回撥錄音時間和錄音檔案完整路徑——在播放的時候需要使用
                    if(mListener!=null){
                        mListener.onFinish(mTime /1000, mAudioManage.getCurrentFilePath());
                    }

                } else if (mCurState == STATE_WANT_TO_CALCEL) {
                    // cancel
                    audioDialogManage.dismissDialog();
                    mAudioManage.cancel();
                }
                reset();
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 恢復狀態以及一些標誌位
     */
    private void reset() {
        isRecordering = false;
        mReady = false;                 //是否觸發onLongClick
        mTime = 0;
        changeState(STATE_NORMAL);
    }

    private boolean wantToCancel(int x, int y) {
        // 判斷手指的滑動是否超出範圍
        if (x < 0 || x > getWidth()) {
            return true;
        }
        if (y < -DISTANCE_Y_CANCEL || y > getHeight() + DISTANCE_Y_CANCEL) {
            return true;
        }
        return false;
    }

    /**
     * 改變Button的背景和文字、展示不同狀態的錄音提示對話方塊
     * @param state
     */
    private void changeState(int state) {
        if (mCurState != state) {
            mCurState = state;
            switch (state) {
                case STATE_NORMAL:
                    setBackgroundResource(R.drawable.send_speech_btn_normal_style);
                    setText(R.string.push_to_speak);
                    break;

                case STATE_RECORDERING:
                    setBackgroundResource(R.drawable.send_speech_btn_pres_style);
                    setText(R.string.release_to_send);
                    if (isRecordering) {
                        // 更新Dialog.recording()
                        audioDialogManage.recording();
                    }
                    break;

                case STATE_WANT_TO_CALCEL:
                    setBackgroundResource(R.drawable.send_speech_btn_pres_style);
                    setText(R.string.release_to_cancel_send);
                    // 更新Dialog.wantCancel()
                    audioDialogManage.wantToCancel();
                    break;
            }
        }
    }

    /*
     * 實現“準備完畢”介面
     * (non-Javadoc)
     */
    @Override
    public void wellPrepared() {
        // TODO Auto-generated method stub
        mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
    }

    private static final int MSG_AUDIO_PREPARED = 0x110;   //準備完全
    private static final int MSG_CURRENT_TIME = 0x111;     //當前語音時長
    private static final int MSG_DIALOG_DISMISS = 0x112;    //銷燬對話方塊
    private static final int MSG_COUNT_DOWN_DONE = 0x113;    //錄音倒計時結束

    /**
     * 接收子執行緒資料,並用此資料配合主執行緒更新UI
     * Handler執行在主執行緒(UI執行緒)中,它與子執行緒通過Message物件傳遞資料。
     * Handler接受子執行緒傳過來的(子執行緒用sedMessage()方法傳弟)Message物件,把這些訊息放入主執行緒佇列中,配合主執行緒進行更新UI。
     */
    private Handler mHandler = new Handler() {

        public void handleMessage(android.os.Message msg) {
            switch (msg.what) {
                case MSG_AUDIO_PREPARED:        //216:mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
                    audioDialogManage.showRecorderingDialog();
                    isRecordering = true;
                    //已經在錄製,同時開啟一個獲取音量、並且計時的執行緒
                    new Thread(mUpdateCurTimeRunnable).start();
                    break;

                case MSG_CURRENT_TIME:          //265:mHandler.sendEmptyMessage(MSG_VOICE_CHANGE);
                    audioDialogManage.updateCurTime(TimeUtils.countDown(mTime));
                    break;

                //這裡在Handler裡面處理DIALOG_DIMISS,是因為想讓該對話方塊顯示一段時間,延遲關閉,——詳見125行
                case MSG_DIALOG_DISMISS:         //125:mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS, 1300);
                    audioDialogManage.dismissDialog();
                    break;
                //處理錄音時間結束
                case MSG_COUNT_DOWN_DONE:
                    mAudioManage.release();
                    // callbackToAct
                    // 正常錄製結束,回撥錄音時間和錄音檔案完整路徑——在播放的時候需要使用
                    if(mListener!=null){
                        mListener.onFinish(mTime /1000, mAudioManage.getCurrentFilePath());
                    }
                    audioDialogManage.dismissDialog();
                    reset();
                    break;
            }
        }
    };

    private int mTime;  //開始錄音計時,計時;(在reset()中置空) 單位為毫秒
    /**
     * 更新當前錄音時長的runnable
     */
    private Runnable mUpdateCurTimeRunnable = new Runnable() {

        @Override
        public void run() {

            while (isRecordering) {
                try {
                    Thread.sleep(100);
                    mTime += 100;
                    mHandler.sendEmptyMessage(MSG_CURRENT_TIME);

                    if(mTime == 60 * 1000){
                        mHandler.sendEmptyMessage(MSG_COUNT_DOWN_DONE);
                    }
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            }
        }
    };
}

接下來是AudioDialogManage:
/**
 * 錄製語音彈窗管理類
 */

public class AudioDialogManage {
    private Dialog mDialog;
    public ImageView mIcon;     //麥克風及刪除圖示
    private TextView mTime;     //錄音時長
    private TextView mLabel;    //錄音提示文字
    private Context mContext;


    public AudioDialogManage(Context context) {
        this.mContext = context;
    }

    /**
     * 預設的對話方塊的顯示
     */
    public void showRecorderingDialog() {
        mDialog = new Dialog(mContext, R.style.Theme_AudioDialog);

        LayoutInflater inflater = LayoutInflater.from(mContext);
        View view = inflater.inflate(
                R.layout.voicenotes_recorder_dialog, null);
        mDialog.setContentView(view);

        mIcon = (ImageView) mDialog.findViewById(R.id.recorder_dialog_icon);
        mTime = (TextView) mDialog.findViewById(R.id.recorder_dialog_time_tv);
        mLabel = (TextView) mDialog.findViewById(R.id.recorder_dialog_label);

        mDialog.show();
    }

    //下面在顯示各種對話方塊時,mDialog已經被構造,只需要控制ImageView、TextView的顯示即可
    /**
     * 正在錄音時,Dialog的顯示
     */
    public void recording() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.VISIBLE);
            mTime.setVisibility(View.VISIBLE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.record_microphone);
            mLabel.setBackgroundColor(Color.parseColor("#00000000"));
            mLabel.setText(R.string.slide_up_cancel_send);
        }
    }

    /**
     * 取消錄音提示對話方塊
     */
    public void wantToCancel() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.VISIBLE);
            mTime.setVisibility(View.GONE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.delete_speech_anim_list);
            mLabel.setBackgroundColor(Color.parseColor("#AF2831"));
            mLabel.setText(R.string.release_to_cancel_send);
        }
    }

    /**
     * 錄音時間過短
     */
    public void tooShort() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.VISIBLE);
            mTime.setVisibility(View.GONE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.speech_is_too_short);
            mLabel.setBackgroundColor(Color.parseColor("#00000000"));
            mLabel.setText("說話時間太短");
        }
    }

    /**
     * mDialog.dismiss();
     */
    public void dismissDialog() {
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss();
            mDialog = null;
        }
    }

    /**
     * 更新顯示當前錄音秒數
     * @param time
     */
    public void updateCurTime(String time) {
        if (mDialog != null && mDialog.isShowing()) {

            mTime.setText(time);
        }
    }
}

提示框的xml程式碼:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:background="@drawable/record_microphone_bj"
        android:layout_width="140dp"
        android:layout_height="140dp"
        android:gravity="center"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:layout_centerInParent="true"
                android:id="@+id/recorder_dialog_icon"
                android:layout_width="55dp"
                android:layout_height="65dp"
                android:src="@drawable/record_microphone"
                android:visibility="visible" />


            <TextView
                android:id="@+id/recorder_dialog_time_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="15sp"
                android:layout_alignBottom="@id/recorder_dialog_icon"
                android:layout_toRightOf="@id/recorder_dialog_icon"
                android:textColor="@color/white"
                android:text="60''"/>

        </RelativeLayout>

        <TextView
            android:id="@+id/recorder_dialog_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:textSize="14sp"
            android:gravity="center"
            android:paddingLeft="5dp"
            android:paddingRight="5dp"
            android:text="@string/slide_up_cancel_send"
            android:textColor="@color/white" />

    </LinearLayout>

</LinearLayout>

最後是AudioManage程式碼:
/**
 * Audio管理類
 */

public class AudioManage {
    private MediaRecorder mMediaRecorder;  //MediaRecorder可以實現錄音和錄影。需要嚴格遵守API說明中的函式呼叫先後順序.
    private String mDir;             // 資料夾的名稱
    private String mCurrentFilePath;

    private static AudioManage mInstance;

    private boolean isPrepared; // 標識MediaRecorder準備完畢

    private AudioManage(String dir) {
        mDir = dir;
    }

    private OnAudioStatusUpdateListener audioStatusUpdateListener;

    private long startTime;

    /**
     * 回撥“準備完畢”
     * @author songshi
     *
     */
    public interface AudioStateListener {
        void wellPrepared();    // prepared完畢
    }

    public AudioStateListener mListener;

    public void setOnAudioStateListener(AudioStateListener audioStateListener) {
        mListener = audioStateListener;
    }


    /**
     * 使用單例實現 AudioManage
     * @param dir
     * @return
     */
    //DialogManage主要管理Dialog,Dialog主要依賴Context,而且此Context必須是Activity的Context,
    //如果DialogManage寫成單例實現,將是Application級別的,將無法釋放,容易造成記憶體洩露,甚至導致錯誤
    public static AudioManage getInstance(String dir) {
        if (mInstance == null) {
            synchronized (AudioManage.class) {   // 同步
                if (mInstance == null) {
                    mInstance = new AudioManage(dir);
                }
            }
        }

        return mInstance;
    }

    /**
     * 準備錄音
     */
    public void prepareAudio() {

        try {
            isPrepared = false;

            File dir = new File(mDir);
            if (!dir.exists()) {
                dir.mkdirs();
            }

            String fileName = GenerateFileName(); // 檔名字
            File file = new File(dir, fileName);  // 路徑+檔名字

            //MediaRecorder可以實現錄音和錄影。需要嚴格遵守API說明中的函式呼叫先後順序.
            mMediaRecorder = new MediaRecorder();
            mCurrentFilePath = file.getAbsolutePath();
            mMediaRecorder.setOutputFile(file.getAbsolutePath());    // 設定輸出檔案
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);    // 設定MediaRecorder的音訊源為麥克風
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);    // 設定音訊的格式
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);    // 設定音訊的編碼為AMR_NB

            mMediaRecorder.prepare();

            mMediaRecorder.start();
            startTime = System.currentTimeMillis();
            updateMicStatus();

            isPrepared = true; // 準備結束

            if (mListener != null) {
                mListener.wellPrepared();
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 生成檔名稱
     * @return
     */
    private String GenerateFileName() {
        // TODO Auto-generated method stub
//        return UUID.randomUUID().toString() + ".amr"; // 音訊檔案格式 ,隨機生成名字
        return TimeUtils.getCurrentTime() + ".amr";     // 生成帶有時間的名字

    }

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

    /**
     * 取消(釋放資源+刪除檔案)
     */
    public void cancel() {

        release();

        if (mCurrentFilePath != null) {
            File file = new File(mCurrentFilePath);
            file.delete();    //刪除錄音檔案
            mCurrentFilePath = null;
        }
    }

    public String getCurrentFilePath() {
        // TODO Auto-generated method stub
        return mCurrentFilePath;
    }

    private int BASE = 1;
    private int SPACE = 100;// 間隔取樣時間

    public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
        this.audioStatusUpdateListener = audioStatusUpdateListener;
    }

    private final Handler mHandler = new Handler();
    private Runnable mUpdateMicStatusTimer = new Runnable() {
        public void run() {
            updateMicStatus();
        }
    };

    /**
     * 更新麥克狀態
     */
    private void updateMicStatus() {

        if (mMediaRecorder != null) {
            double ratio = (double)mMediaRecorder.getMaxAmplitude() / BASE;
            double db;// 分貝
            if (ratio > 1) {
                db = 20 * Math.log10(ratio);
                if(null != audioStatusUpdateListener) {
                    audioStatusUpdateListener.onUpdate(db,System.currentTimeMillis()-startTime);
                }
            }
            mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
        }
    }

    public interface OnAudioStatusUpdateListener {
        /**
         * 錄音中...
         * @param db 當前聲音分貝
         * @param time 錄音時長
         */
        public void onUpdate(double db,long time);

    }
}

然後再放上Dialog的style程式碼:
<!-- 錄音對話方塊 <style name="Theme_AudioDialog"> -->
    <style name="Theme_AudioDialog" parent="@android:style/Theme.Dialog">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowFrame">@null</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:backgroundDimEnabled">false</item>
    </style>

最後呢記得在使用的過程中呼叫以下方法:
//傳送錄音結束介面
        sendSpeechBtn.setAudioFinishRecorderListener(new AudioRecorderButton.AudioFinishRecorderListener() {

            @Override
            public void onFinish(int seconds, String FilePath) {

                ToastUtils.showShort(ChattingActivity.this,"語音檔案為:"+FilePath+"時長:"+seconds);
                //拿到檔案地址和時長後就可以去做傳送語音的操作了
                
            }
        });
以上就是今天跟大家分享的語音功能的全部程式碼了,目前還未完成將傳送的語音檔案顯示到列表上以及點選list  Item之後可以去播放語音檔案,因為急著要去完成別的功能,這個就過幾天再去實現。需要原始碼的話就說一下,我好先整理整理,畢竟全部寫在了公司的專案中。如果你在使用的過程中遇到問題歡迎私信我,說不定我們之間還能發生些什麼,嘿嘿嘿~(●ˇ∀ˇ●)

對了,要實現麥克風圖形可以波動的效果記得使用類似這樣的圖片


好了,就先寫到這裡了。