Android仿微信语音聊天功能

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

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

Android仿微信语音聊天功能_第1张图片

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

首先讨论录音对话框,共分4种情况。

- 1、默认(不显示对话框)

- 2、正在录音(显示麦克风和音量)

- 3、试图取消(显示箭头)

- 4、时间过短(显示叹号)

根据上面分析,先写出对话框的布局。对话框上排为两张图片,下面为一行提示文字


<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文件中,加上对话框的样式

接下来创建一个用于管理对话框的类——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

你可能感兴趣的:(Android自定义控件)