Android仿微信语音聊天功能
2015-11-13 00:16
741 查看
本文是仿照张鸿洋在慕课网的教学视频《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
相关文章推荐
- Android仿微信朋友圈图片查看器
- 微信公众号通讯录同步
- 微信公众号开发之天气应用
- 微信公众号开发---微信请求服务端取值问题
- 微信公众平台开发(三)
- 微信按钮之联系我们
- 微信发送客服接口
- [实例]JAVA调用微信接口发送图文消息,不用跳到详情页
- 微信公众号开发创建菜单栏(二)
- 腾讯优测自动化测试场景丨iBeacon走进微信“豪门”
- 微信绑定后台是验证token失败
- android微信登录,分享
- android微信登录,分享
- android微信登录,分享
- 微信支付iOS
- 高仿微信6.0Tab bar
- 微信企业号开发相关问题
- 微信JS分享实战代码
- C#开发微信门户及应用(1)--开始使用微信接口
- Testlink自动执行用例小程序