Android仿微信语音聊天功能

来源:互联网 发布:淘宝电子商务运营 编辑:程序博客网 时间:2024/05/15 01:34

本文是仿照张鸿洋在慕课网的教学视频《Android-仿微信语音聊天》而作,从某种意义上来说并不能算作纯粹的原创,在此首先向这位大神致敬~

首先展示一下效果。1、当用户按下“按住说话”按钮时,弹出对话框,此时开始录音,并且右边的音量随声音大小而波动。2、如果这时手指向上滑动,则显示取消发送语音的提示。3、当录音结束时,发送语音。4、如果录音时间过短,则对话框给出提示,此次录音失效。

效果展示

实现此功能的关键在于三个部分:提示对话框,声音录制和录音按钮。

首先讨论录音对话框,共分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() / 32768 + 1;            } catch (Exception e) {}        }        return 1;    }

录音可能被用户取消,也可能是正常的录制结束,因此还需要提供这两个方法。它们的差别在于正常录制结束时需要保留下录音文件,而取消录音时不用。

    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 = 50;    //最大声音级别    private static final int MAX_VOLUME_LEVEL = 7;    //最短录音时长    private static final float LEAST_REC_TIME = 1.0f;    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 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 = 0x100;    private static final int MSG_VOICE_CHANGED = 0x101;    private static final int MSG_DIALOG_DISMISS = 0x102;    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(100);                    mTime += 0.1f;                    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, 1300);                    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,1300);                    }                    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

2 1
原创粉丝点击