天天看點

Android仿微信語音聊天功能

本文是仿照張鴻洋在慕課網的教學視訊《Android-仿微信語音聊天》而作,從某種意義上來說并不能算作純粹的原創,在此首先向這位大神緻敬~

首先展示一下效果。1、當使用者按下“按住說話”按鈕時,彈出對話框,此時開始錄音,并且右邊的音量随聲音大小而波動。2、如果這時手指向上滑動,則顯示取消發送語音的提示。3、當錄音結束時,發送語音。4、如果錄音時間過短,則對話框給出提示,此次錄音失效。

Android仿微信語音聊天功能

實作此功能的關鍵在于三個部分:提示對話框,聲音錄制和錄音按鈕。

首先讨論錄音對話框,共分4種情況。

- 1、預設(不顯示對話框)

- 2、正在錄音(顯示麥克風和音量)

- 3、試圖取消(顯示箭頭)

- 4、時間過短(顯示歎号)

根據上面分析,先寫出對話框的布局。對話框上排為兩張圖檔,下面為一行提示文字

<?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:padding="20dp"
    android:gravity="center"
    android:background="@drawable/audiorec_dialog_loading_bg"
    android:orientation="vertical">

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

        <ImageView
            android:id="@+id/img_recdlg_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/audiorec_recorder"
            android:visibility="visible"/>

        <ImageView
            android:id="@+id/img_recdlg_voice"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/audiorec_v1"
            android:visibility="visible"/>

    </LinearLayout>

    <TextView
        android:id="@+id/txt_recdlg_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:text="@string/str_audiorecdlg_label_recording"
        android:textColor="@color/white"/>

</LinearLayout>
           

并且在styles.xml檔案中,加上對話框的樣式

<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:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowIsTranslucent">true</item>    <!--半透明-->
        <item name="android:backgroundDimEnabled">false</item>  <!--背景變暗-->
</style>
           

接下來建立一個用于管理對話框的類——RecordDialogManager,并且在類中提供外部調用的方法,使其能夠轉換成上面說的4中情況。預設狀态下,直接把dialog給dismiss掉即可。對于正在錄音這種情況,首先我們要建立顯示對話框,然後将圖檔設為對應樣式。

public void showRecordingDialog()
    {
        mDialog = new Dialog(mContext, R.style.Theme_AudioDialog);
        LayoutInflater inflater = LayoutInflater.from(mContext);
        View view = inflater.inflate(R.layout.layout_dialog_rec,null);
        mDialog.setContentView(view);

        mIcon = (ImageView) mDialog.findViewById(R.id.img_recdlg_icon);
        mVoice = (ImageView) mDialog.findViewById(R.id.img_recdlg_voice);
        mLabel = (TextView) mDialog.findViewById(R.id.txt_recdlg_label);

        mDialog.show();
    }

    public void recording()
    {
        if (mDialog != null && mDialog.isShowing())
        {
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.VISIBLE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.audiorec_recorder);
            mLabel.setText(R.string.str_audiorecdlg_label_recording);
        }
    }
           

錄音過程中,需要動态改變顯示音量的大小,是以還需要提供一個調用方法,以改變音量值。這裡通過音量值,組成資源引用的名稱,然後加載對應的圖檔。

/**
     * 更新聲音級别的圖檔
     * @param level must be 1-7
     */
    public void updateVoiceLevel(int level)
    {
        if (mDialog != null && mDialog.isShowing())
        {
            //通過level擷取resId
            int resId = mContext.getResources().getIdentifier("audiorec_v"+level,
                    "drawable",mContext.getPackageName());
            mVoice.setImageResource(resId);
        }
    }
           

試圖取消錄音時,需要換掉圖檔,并且隻顯示一張圖。錄音過短與之類似。

public void wangToCancel()
    {
        if (mDialog != null && mDialog.isShowing())
        {
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.GONE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.audiorec_cancel);
            mLabel.setText(R.string.str_audiorecbtn_want_cancel);
        }
    }

    public void tooShort()
    {
        if (mDialog != null && mDialog.isShowing())
        {
            mIcon.setVisibility(View.VISIBLE);
            mVoice.setVisibility(View.GONE);
            mLabel.setVisibility(View.VISIBLE);

            mIcon.setImageResource(R.drawable.audiorec_voice_too_short);
            mLabel.setText(R.string.str_audiorecdlg_label_too_short);
        }
    }
           

當然,文字也要換成對應的。

<string name="str_audiorecbtn_want_cancel">松開手指,取消發送</string>
    <string name="str_audiorecdlg_label_recording">手指上滑,取消發送</string>
    <string name="str_audiorecdlg_label_too_short">錄音時間過短</string>
           

接下來是聲音錄制子產品,使用MediaRecorder這個類實作錄音,并且向外部提供幾個方法,用于錄音過程的控制。由于我們不希望出現多個錄音的執行個體,是以這個類設為單例模式。

首先是準備錄音,這裡做一些初始化的操作,并且在完成之後要告知界面準備完畢,以便在界面上顯示正在錄音的對話框。是以,要提供一個接口,并在準備完成後調用。

public void prepareAudio()      //準備
    {
        String strPath = MediaManager.getInstance().getStoragePath(MediaManager.MediaType.AUDIO_UPLOAD);
        String fileName = "voice_"+System.currentTimeMillis()+".amr";
        curFile = new File(strPath,fileName);

        isPrepared = false;
        recorder = new MediaRecorder();
        recorder.setOutputFile(curFile.getAbsolutePath());

        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);         //音頻源為麥克風
        recorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);    //輸出檔案格式
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);    //音頻編碼格式

        try 
        {
            recorder.prepare();
            recorder.start();       //已經準備好
            isPrepared = true;
            if( null != listener )
            {
                listener.onPrepared();
            }

        } 
        catch (IOException e) 
        {
            e.printStackTrace();
        }
    }
    //接口
    private AudioStateListener listener;
    public interface AudioStateListener
    {
        void onPrepared();      //回調 準備完畢
    }
    public void setOnAudioStateListener( AudioStateListener listener)
    {
        this.listener = listener;
    }
           

在錄音開始之後,需要不斷的擷取目前的音量,是以需要提供擷取音量的方法

public int getVoiceVolume( int maxLevel )     //音量等級
    {
        if( isPrepared )
        {
            try {
                //振幅範圍是 1-32767
                return maxLevel * recorder.getMaxAmplitude() /  + ;
            } catch (Exception e) {}
        }
        return ;
    }
           

錄音可能被使用者取消,也可能是正常的錄制結束,是以還需要提供這兩個方法。它們的差别在于正常錄制結束時需要保留下錄音檔案,而取消錄音時不用。

public void release()       //釋放
    {
        recorder.stop();
        recorder.release();
        recorder = null;
    }

    public void cancel()        //取消
    {
        release();
        if( null != curFile )
        {
            curFile.delete();
            curFile = null;
        }
    }
           

最後讨論錄音按鈕,這個按鈕總共有三種狀态:未錄音時的狀态(STATE_NORMAL)、正在錄音時的狀态(STATE_RECORDING)和試圖取消錄音(STATE_WANT_TO_CANCEL)。

由于使用者的按下、移動和擡起是操作于這個Button的,是以我們需要記錄使用者的MotionEvent,并以此改變按鈕狀态。

此外,在一次錄制完成之後,需要給Button所在的Activity提供一個回調的方法,讓Activity執行後續的操作(比如上傳語音到伺服器)。

首先我們來定義按鈕的狀态和一些記錄狀态的變量

//Y方向按住移動此距離後更改狀态為試圖取消
    private static final int DISTANCE_Y_CANCEL = ;

    //最大聲音級别
    private static final int MAX_VOLUME_LEVEL = ;
    //最短錄音時長
    private static final float LEAST_REC_TIME = f;

    private static final int STATE_NORMAL = ;
    private static final int STATE_RECORDING = ;
    private static final int STATE_WANT_TO_CANCEL = ;

    private int mCurState = STATE_NORMAL;
    private boolean isRecording;                //錄音準備是否已經完成
    private boolean mReady;                     //是否已經進入錄音狀态
    private float mTime;                        //計時
           

由于我們要接收錄音器準備完成的事件,是以我們需要實作對應的接口,并且在接口回調中顯示對話框(這裡隻寫了定義,還需要給VoiceRecorder設定上這個接口)。

對話框的顯示,這裡用了消息投遞的方法,是以還需要建立一個Handler并處理所有可能的資訊。除了對話框的顯示之外,更新目前音量和關閉對話框也是通過投遞消息來實作的。

VoiceRecorder.AudioStateListener asListener = new VoiceRecorder.AudioStateListener()
    {
        @Override
        public void onPrepared()
        {
            mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
        }

    };

    private static final int MSG_AUDIO_PREPARED = ;
    private static final int MSG_VOICE_CHANGED = ;
    private static final int MSG_DIALOG_DISMISS = ;

    private Handler mHandler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MSG_AUDIO_PREPARED:
                    mDialogManager.showRecordingDialog();
                    isRecording = true;
                    new Thread(mGetVolume).start();    //開啟新線程,記錄錄音時間,并不斷擷取音量
                    break;
                case MSG_VOICE_CHANGED:
                    mDialogManager.updateVoiceLevel(
                        VoiceRecorder.getInstance().getVoiceVolume(MAX_VOLUME_LEVEL));
                    break;
                case MSG_DIALOG_DISMISS:
                    mDialogManager.dismissDialog();
                    break;
            }
        }
    };

    //擷取音量大小
    private Runnable mGetVolume = new Runnable()
    {
        @Override
        public void run()
        {
            while ( isRecording )
            {
                try
                {
                    Thread.sleep();
                    mTime += f;
                    mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    };
           

接下來我們定義在不同狀态下,按鈕和對話框的更新。

private void changeState(int state)
    {
        if (mCurState != state)
        {
            mCurState = state;
            switch (state)
            {
                case STATE_NORMAL:
                    setBackgroundResource(R.drawable.im_controlbar_inputbox_n);
                    setText(R.string.str_audiorecbtn_normal);
                    break;
                case STATE_RECORDING:
                    if(mReady == false)
                    {
                        mReady = true;
                        VoiceRecorder.getInstance().prepareAudio();
                    }
                    setBackgroundResource(R.drawable.im_controlbar_inputbox_p);
                    setText(R.string.str_audiorecbtn_recording);
                    if (isRecording)
                    {
                        mDialogManager.recording();
                    }
                    break;
                case STATE_WANT_TO_CANCEL:
                    setBackgroundResource(R.drawable.im_controlbar_inputbox_p);
                    setText(R.string.str_audiorecbtn_want_cancel);
                    mDialogManager.wangToCancel();
                    break;
                default:
                    break;
            }
        }
    }
           

然後是最關鍵的部分,通過MotionEvent來更改按鈕的目前狀态,是以要覆寫onTouchEvent方法。由于在按下之後,可能最終要取消錄音,是以需要在按下後,使用者移動手指時,獲得目前的坐标。

@Override
    public boolean onTouchEvent(MotionEvent event)
    {
        int action = event.getAction();
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:        
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
           
當按下時,一次錄音開始,更改狀态為STATE_RECORDING
case MotionEvent.ACTION_DOWN:
                reset();
                changeState(STATE_RECORDING);
                break;
           
當移動手指時,需要檢測是否已經進入或越出了試圖取消錄音的範圍,并以此來更新狀态
case MotionEvent.ACTION_MOVE:
                if (isRecording)
                {
                    //根據坐标判斷是否想要取消
                    if (wantToCancel(x, y))
                    {
                        changeState(STATE_WANT_TO_CANCEL);
                    }
                    else
                    {
                        changeState(STATE_RECORDING);
                    }
                }
                break;
           
接下來是難點,當松開手指後,需要分以下幾種情況讨論
-1、如果按下之後立刻擡起手指,狀态還沒有切換到STATE_RECORDING(雖然幾乎不可能)
-2、狀态切換到STATE_RECORDING,但是AudioRecorder還沒準備完成
-3、AudioRecorder準備完成,但是錄音時間太短
-4、正常錄音結束
-5、使用者取消錄音

據此寫出對于ACTION_UP的處理

case MotionEvent.ACTION_UP:
                if(!mReady)     //狀态還沒切換
                {
                    reset();
                    mDialogManager.showRecordingDialog();
                    mDialogManager.tooShort();
                    mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS, );
                    return super.onTouchEvent(event);
                }
                if( !isRecording || mTime < LEAST_REC_TIME )    //prepare還沒完成 或 錄音時間太短
                {
                    VoiceRecorder.getInstance().cancel();
                    if(STATE_RECORDING == mCurState)
                    {
                        mDialogManager.tooShort();
                        mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS,);
                    }
                    else
                    {
                        mDialogManager.dismissDialog();
                    }
                }
                else if (STATE_RECORDING == mCurState)      //正常錄制結束
                {
                    mDialogManager.dismissDialog();
                    VoiceRecorder.getInstance().release();
                    if( listener != null)
                    {
                        listener.onRecordFinish(mTime,VoiceRecorder.getInstance().getFilePath());
                    }
                }
                else if (STATE_WANT_TO_CANCEL == mCurState)     //取消錄音
                {
                    mDialogManager.dismissDialog();
                    VoiceRecorder.getInstance().cancel();
                }
                reset();
                break;
           

前文說過,在一次錄制完成之後,需要給按鈕所在的Activity提供一個回調的方法,是以定義一個接口

//錄音完成回調接口
    public interface OnRecordFinishListener
    {
        void onRecordFinish(float seconds, String fileName);
    }
    private OnRecordFinishListener listener;
    public void setOnRecordFinishListener( OnRecordFinishListener listener )
    {
        this.listener = listener;
    }
           

至此,錄音按鈕這個類基本上就完成了。

當然,聲音錄下來了最終是為了播放,是以我們還需要寫一個類用于播放聲音,這個用MediaPlayer實作就可以,沒什麼過多強調的,直接上代碼了。

public class MediaManager 
{
    private static MediaManager mInstance;
    private static final String AUDIO_DIR = "/im/audio";
    private static final String AUDIO_UPLOAD_DIR = "/im/audio/upload";

    private MediaManager() {}
    public static MediaManager getInstance() 
    {
        if (null == mInstance) 
        {
            synchronized (MediaManager.class) 
            {
                if (null == mInstance) 
                {
                    mInstance = new MediaManager();
                }
            }
        }
        return mInstance;
    }

    private MediaPlayer mMediaPlayer;

    private boolean isPause;     //目前是否暫停

    public String getStoragePath(MediaType type)
    {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) 
        {
            String sdcardPath = Environment.getExternalStorageDirectory().getAbsolutePath();
            File dir = null;
            switch (type) 
            {
                case AUDIO:
                    dir = new File(sdcardPath + AUDIO_DIR);
                    break;
                case AUDIO_UPLOAD:
                    dir = new File(sdcardPath + AUDIO_UPLOAD_DIR);
                    break;
            }
            if (!dir.exists()) 
            {
                dir.mkdirs();
            }
            return dir.getAbsolutePath();
        } 
        else 
        {
            return null;
        }

    }

    public void playSound(String filePath, MediaPlayer.OnCompletionListener listener) 
    {
        if (null == mMediaPlayer) 
        {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() 
            {
                @Override
                public boolean onError(MediaPlayer mp, int what, int extra) 
                {
                    mMediaPlayer.reset();
                    return false;
                }
            });
        } 
        else 
        {
            mMediaPlayer.reset();
        }

        try 
        {
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mMediaPlayer.setOnCompletionListener(listener);
            mMediaPlayer.setDataSource(filePath);
            mMediaPlayer.prepare();
            mMediaPlayer.start();
        } 
        catch (IOException e) 
        {
            e.printStackTrace();
        }

    }

    public void pause() 
    {
        if (null != mMediaPlayer && mMediaPlayer.isPlaying()) 
        {
            mMediaPlayer.pause();
            isPause = true;
        }
    }

    public void resume() 
    {
        if (null != mMediaPlayer && isPause) 
        {
            mMediaPlayer.start();
            isPause = false;
        }
    }

    public void release() 
    {
        if (null != mMediaPlayer) 
        {
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }

    public enum MediaType 
    {
        AUDIO,
        AUDIO_UPLOAD,
    }
}
           

我們在錄音按鈕那個類裡面給Activity提供了一個回調方法,Activity中隻需實作這個接口,并完成後續操作即可。

AudioRecorderButton.OnRecordFinishListener orfListener = new AudioRecorderButton.OnRecordFinishListener() 
    {
        @Override
        public void onRecordFinish(float seconds, String fileName)  
        {
            uploadAudio(new File(fileName), Math.round(seconds), new Callback() 
            {
                @Override
                public void onFailure(Exception e)  
                {

                }

                @Override
                public void onSuccess()  
                {
                    //其他操作,添加到listview等等
                }
            });
        }
    };
           

源碼下載下傳連結:http://download.csdn.net/detail/liusiqian0209/9265237