最近公司项目中需要用到聊天功能,在通过对比网易云信和环信之后呢还是选择了使用环信。虽然我很喜欢网易的云音乐,但环信使用起来确实比较简单,就选择了它,今天这篇文章主要是讲的关于语音聊天的功能的实现。先来看一下我做的效果图吧~
由于为了录制这个动态图,我用的是模拟器,所以在录制过程中的麦克风中间的波形没有波动,但是使用真机的话麦克风的波形是可以随着说话的音量波动起来的。在用户松开按钮后将录音文件保存在本地,没做其他操作,如果你也和我一样正在做这一块的话可以尝试去将这个功能去完善起来。
首先呢来整理一下思路,用户在按住按钮之后会进行调用麦克风进行录音,松开按钮后将语音文件保存在本地。虽然看起来比较简单,但是为了实现这个功能,我也参考了其他大神的方法,也花了不少时间,觉得写的不错的话,就点个赞呗,嘿嘿嘿。~
闲话不多说,现在来看看我是如何实现的吧。
一共写了三个类:
首先自定义了一个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();
}
}
}
};
}
/**
* 录制语音弹窗管理类
*/
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);
}
}
}
/**
* 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);
}
}
//发送录音结束接口
sendSpeechBtn.setAudioFinishRecorderListener(new AudioRecorderButton.AudioFinishRecorderListener() {
@Override
public void onFinish(int seconds, String FilePath) {
ToastUtils.showShort(ChattingActivity.this,"语音文件为:"+FilePath+"时长:"+seconds);
//拿到文件地址和时长后就可以去做发送语音的操作了
}
});
对了,要实现麦克风图形可以波动的效果记得使用类似这样的图片
好了,就先写到这里了。