最近项目中做了一个聊天的功能,为了和Web端的配合,用WebSocket实现了文字聊天和单张图片的发送聊天,参见:http://blog.csdn.net/lijinweii/article/details/73551370 ,但是在实现语音聊天的时候发了难里了,苦思无果,果断在网上查了下找到了篇“仿微信语音聊天”的功能。看了以后顿时心花怒放,这不就是我所需要吗。。。
哈哈哈,看下效果图啊:
他的功能和微信语音聊天及其相似。
看下代码:
AudioManager.java:
public class AudioManager {
private MediaRecorder mRecorder;
private String mDirString;
private String mCurrentFilePathString;
private boolean isPrepared;// 是否准备好了
/**
* 单例化的方法 1 先声明一个static 类型的变量a 2 在声明默认的构造函数 3 再用public synchronized static
* 类名 getInstance() { if(a==null) { a=new 类();} return a; } 或者用以下的方法
*/
/**
* 单例化这个类
*/
private static AudioManager mInstance;
private AudioManager(String dir) {
mDirString = dir;
}
public static AudioManager getInstance(String dir) {
if (mInstance == null) {
synchronized (AudioManager.class) {
if (mInstance == null) {
mInstance = new AudioManager(dir);
}
}
}
return mInstance;
}
/**
* 回调函数,准备完毕,准备好后,button才会开始显示录音框
* @author nickming
*/
public interface AudioStageListener {
void wellPrepared();
}
public AudioStageListener mListener;
public void setOnAudioStageListener(AudioStageListener listener) {
mListener = listener;
}
// 准备方法
public void prepareAudio() {
try {
// 一开始应该是false的
isPrepared = false;
// Public File(String path)
// 参数为String,构造一个新的File使用指定的路径
File dir = new File(mDirString);
if (!dir.exists()) {
/* mkdir():只能创建一层目录.
如: File file = new File("c:/aa");file.mkdir(); //这样可以在c:/下创建aa目录
假如有多层目录,则不能用mkdir创建:File file = new File("c:/aa/bb"); file.mkdir() //这样创建不了.
应该改为:mkdirs():*/
dir.mkdirs();
}
//随机生成文件的名称
String fileNameString = generalFileName();
// public File(File dir,String name)
// 参数为File和String,File指定构造的新的File对象的路径(文件路径),而String指定新的File名字(文件名称)
File file = new File(dir, fileNameString);
mCurrentFilePathString = file.getAbsolutePath();//获取录音文件的路径
mRecorder = new MediaRecorder();
// 设置输出文件
mRecorder.setOutputFile(file.getAbsolutePath());
// 设置meidaRecorder的音频源是麦克风
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 设置文件音频的输出格式为amr
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
// 设置音频的编码格式为amr
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
// 严格遵守google官方api给出的mediaRecorder的状态流程图
mRecorder.prepare();
mRecorder.start();
// 准备结束
isPrepared = true;
// 已经准备好了,可以录制了
if (mListener != null) {
mListener.wellPrepared();
}
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 随机生成文件的名称
*
* @return
*/
private String generalFileName() {
// TODO Auto-generated method stub
return UUID.randomUUID().toString() + ".amr";
}
// 获得声音的level
public int getVoiceLevel(int maxLevel) {
// mRecorder.getMaxAmplitude()这个是音频的振幅范围,值域是1-32767
if (isPrepared) {
try {
// 取证+1,否则去不到7
return maxLevel * mRecorder.getMaxAmplitude() / 32768 + 1;
} catch (Exception e) {
// TODO Auto-generated catch block
}
}
return 1;
}
// 释放资源
public void release() {
// 严格按照api流程进行
mRecorder.stop();
mRecorder.release();
mRecorder = null;
}
// 取消,因为prepare时产生了一个文件,所以cancel方法应该要删除这个文件,
// 这是与release的方法的区别
public void cancel() {
release();
if (mCurrentFilePathString != null) {
File file = new File(mCurrentFilePathString);
file.delete();
mCurrentFilePathString = null;
}
}
//录音的文件路径
public String getCurrentFilePath() {
// TODO Auto-generated method stub
return mCurrentFilePathString;
}
}
AudioRecorderButton.java:
public class AudioRecordButton extends Button implements AudioManager.AudioStageListener {
private static final int STATE_NORMAL = 1;// 默认的状态
private static final int STATE_RECORDING = 2;// 正在录音
private static final int STATE_WANT_TO_CANCEL = 3;// 希望取消
private static final int DISTANCE_Y_CANCEL = 50;
private int mCurrentState = STATE_NORMAL;// 当前的状态
private boolean isRecording = false; // 已经开始录音
private DialogManager mDialogManager;
private AudioManager mAudioManager;
private float mTime = 0;
// 是否触发了onlongclick,准备好了
private boolean mReady;
/**
* 先实现两个参数的构造方法,布局会默认引用这个构造方法, 用一个 构造参数的构造方法来引用这个方法 * @param context
*/
public AudioRecordButton(Context context) {
this(context, null);
// TODO Auto-generated constructor stub
}
public AudioRecordButton(final Context context, AttributeSet attrs) {
super(context, attrs);
mDialogManager = new DialogManager(getContext());
// 这里没有判断储存卡是否存在,有空要判断
String dir = Environment.getExternalStorageDirectory() + "/nickming_recorder_audios";
mAudioManager = AudioManager.getInstance(dir);
mAudioManager.setOnAudioStageListener(this);
// 由于这个类是button所以在构造方法中添加监听事件
setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
// TODO Auto-generated method
mReady = true;
// 准备方法
mAudioManager.prepareAudio();
return false;
}
});
// TODO Auto-generated constructor stub
}
/**
* 录音完成后的回调,回调给activiy,可以获得mtime和文件的路径
*
* @author nickming
*/
public interface AudioFinishRecorderListener {
void onFinished(float seconds, String filePath);
}
private AudioFinishRecorderListener mListener;
public void setAudioFinishRecorderListener(AudioFinishRecorderListener listener) {
mListener = listener;
}
// 获取音量大小的runnable
private Runnable mGetVoiceLevelRunnable = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (isRecording) {// 开始录音
try {
Thread.sleep(100);
mTime += 0.1f;
mhandler.sendEmptyMessage(MSG_VOICE_CHANGE);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
// 准备三个常量
private static final int MSG_AUDIO_PREPARED = 0X110;
private static final int MSG_VOICE_CHANGE = 0X111;
private static final int MSG_DIALOG_DIMISS = 0X112;
private Handler mhandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_AUDIO_PREPARED:
// 显示应该是在audio end prepare之后回调 显示對話框在开始录音以后
mDialogManager.showRecordingDialog();// 显示录音的对话框
isRecording = true;
new Thread(mGetVoiceLevelRunnable).start();//开启一个线程
// 需要开启一个线程来变换音量
break;
case MSG_VOICE_CHANGE:
// 显示更新音量级别的对话框
mDialogManager.updateVoiceLevel(mAudioManager.getVoiceLevel(7));
break;
case MSG_DIALOG_DIMISS:
break;
}
}
;
};
// 在这里面发送一个handler的消息
@Override
public void wellPrepared() {
// TODO Auto-generated method stub
mhandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
}
/**
* 直接复写这个监听函数
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int action = event.getAction();
int x = (int) event.getX();// 获得x轴坐标
int y = (int) event.getY();// 获得y轴坐标
switch (action) {
case MotionEvent.ACTION_DOWN:
changeState(STATE_RECORDING);
break;
case MotionEvent.ACTION_MOVE:
if (isRecording) {
// 根据x,y来判断用户是否想要取消
if (wantToCancel(x, y)) {
changeState(STATE_WANT_TO_CANCEL);
} else {
changeState(STATE_RECORDING);
}
}
break;
case MotionEvent.ACTION_UP:
// 首先判断是否有触发onlongclick事件,没有的话直接返回reset
if (!mReady) {
reset();
return super.onTouchEvent(event);
}
// 如果按的时间太短,还没准备好或者时间录制太短,就离开了,则显示这个dialog
if (!isRecording || mTime < 0.6f) {
mDialogManager.tooShort();
mAudioManager.cancel();
mhandler.sendEmptyMessageDelayed(MSG_DIALOG_DIMISS, 1300);// 持续1.3s
} else if (mCurrentState == STATE_RECORDING) {//正常录制结束
mDialogManager.dimissDialog();
mAudioManager.release();// release释放一个mediarecorder
if (mListener != null) {// 并且callbackActivity,保存录音
mListener.onFinished(mTime, mAudioManager.getCurrentFilePath());//录音多久(时间段), 文件路径
}
} else if (mCurrentState == STATE_WANT_TO_CANCEL) {// 想要取消
// cancel
mAudioManager.cancel();
mDialogManager.dimissDialog();
}
reset();// 恢复标志位
break;
}
return super.onTouchEvent(event);
}
/**
* 回复标志位以及状态
*/
private void reset() {
// TODO Auto-generated method stub
isRecording = false;
changeState(STATE_NORMAL);// 默认的状态
mReady = false;
mTime = 0;
}
private boolean wantToCancel(int x, int y) {
// TODO Auto-generated method stub
if (x < 0 || x > getWidth()) {// 判断是否在左边,右边,上边,下边 // 超过按钮的宽度
return true;
}
if (y < -DISTANCE_Y_CANCEL || y > getHeight() + DISTANCE_Y_CANCEL) { // 超过按钮的高度
return true;
}
return false;
}
/**
* 改变
*/
private void changeState(int state) {
// TODO Auto-generated method stub
if (mCurrentState != state) {
mCurrentState = state;
switch (mCurrentState) {
case STATE_NORMAL:// 默认的状态
setBackgroundResource(R.drawable.button_recordnormal);
setText(R.string.normal);
break;
case STATE_RECORDING:// 正在录音
setBackgroundResource(R.drawable.button_recording);
setText(R.string.recording);
if (isRecording) {
mDialogManager.recording();
// 复写dialog.recording();
}
break;
case STATE_WANT_TO_CANCEL://希望取消
setBackgroundResource(R.drawable.button_recording);
setText(R.string.want_to_cancle);
// dialog want to cancel
mDialogManager.wantToCancel();
break;
}
}
}
@Override
public boolean onPreDraw() {
// TODO Auto-generated method stub
return false;
}
}
DialogManager.java:
//用于管理Dialog
public class DialogManager {
/**
* 以下为dialog的初始化控件,包括其中的布局文件
*/
private Dialog mDialog;
private ImageView mIcon;
private ImageView mVoice;
private TextView mLable;
private Context mContext;
/**
* 构造方法 传入上下文
*/
public DialogManager(Context context) {
// TODO Auto-generated constructor stub
mContext = context;
}
// 显示录音的对话框
public void showRecordingDialog() {
// TODO Auto-generated method stub
mDialog = new Dialog(mContext,R.style.Theme_audioDialog);
// 用layoutinflater来引用布局
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.dialog_manager, null);
mDialog.setContentView(view);
mIcon = (ImageView) mDialog.findViewById(R.id.dialog_icon);
mVoice = (ImageView) mDialog.findViewById(R.id.dialog_voice);
mLable = (TextView) mDialog.findViewById(R.id.recorder_dialogtext);
mDialog.show();
}
/**
* 设置正在录音时的dialog界面
*/
public void recording() {
if (mDialog != null && mDialog.isShowing()) {//显示状态
mIcon.setVisibility(View.VISIBLE);
mVoice.setVisibility(View.VISIBLE);
mLable.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.recorder);
mLable.setText(R.string.shouzhishanghua);
}
}
/**
* 取消界面
*/
public void wantToCancel() { // 显示想取消的对话框
// TODO Auto-generated method stub
if (mDialog != null && mDialog.isShowing()) {//显示状态
mIcon.setVisibility(View.VISIBLE);
mVoice.setVisibility(View.GONE);
mLable.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.cancel);
mLable.setText(R.string.want_to_cancle);
}
}
// 时间过短
public void tooShort() { // 显示时间过短的对话框
// TODO Auto-generated method stub
if (mDialog != null && mDialog.isShowing()) {
mIcon.setVisibility(View.VISIBLE);
mVoice.setVisibility(View.GONE);
mLable.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.voice_to_short);
mLable.setText(R.string.tooshort);
}
}
// 隐藏dialog
public void dimissDialog() {
// 显示取消的对话框
// TODO Auto-generated method stub
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
mDialog = null;
}
}
// 显示更新音量级别的对话框
public void updateVoiceLevel(int level) {
// TODO Auto-generated method stub
if (mDialog != null && mDialog.isShowing()) {
//先不改变它的默认状态
// mIcon.setVisibility(View.VISIBLE);
// mVoice.setVisibility(View.VISIBLE);
// mLable.setVisibility(View.VISIBLE);
//通过level来找到图片的id,也可以用switch来寻址,但是代码可能会比较长
int resId = mContext.getResources().getIdentifier("v" + level,
"drawable", mContext.getPackageName());
//设置图片的id
mVoice.setImageResource(resId);
}
}
}
MediaManager.java
public class MediaManager {
private static MediaPlayer mPlayer;
private static boolean isPause;
public static void playSound(String filePathString, OnCompletionListener onCompletionListener) {
// TODO Auto-generated method stub
if (mPlayer==null) {
mPlayer=new MediaPlayer();
//保险起见,设置报错监听 //设置一个error监听器
mPlayer.setOnErrorListener(new OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// TODO Auto-generated method stub
mPlayer.reset();
return false;
}
});
}else {
mPlayer.reset();//就回复
}
try {
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mPlayer.setOnCompletionListener(onCompletionListener);
mPlayer.setDataSource(filePathString);
mPlayer.prepare();
mPlayer.start();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//停止函数
public static void pause(){
if (mPlayer!=null&&mPlayer.isPlaying()) {
mPlayer.pause();
isPause=true;
}
}
//继续
public static void resume() {
if (mPlayer!=null&&isPause) {
mPlayer.start();
isPause=false;
}
}
/**
* 释放资源
*/
public static void release() {
if (mPlayer!=null) {
mPlayer.release();
mPlayer=null;
}
}
}
RecoderAdapter.java
public class RecorderAdapter extends ArrayAdapter<MainActivity.Recorder> {
private LayoutInflater inflater;
private int mMinItemWith;// 设置对话框的最大宽度和最小宽度 //最小的item宽度
private int mMaxItemWith; //最大的item宽度
public RecorderAdapter(Context context, List<MainActivity.Recorder> dataList) {
super(context, -1, dataList);
// TODO Auto-generated constructor stub
inflater = LayoutInflater.from(context);
// 获取系统宽度
WindowManager wManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wManager.getDefaultDisplay().getMetrics(outMetrics);
mMaxItemWith = (int) (outMetrics.widthPixels * 0.7f);
mMinItemWith = (int) (outMetrics.widthPixels * 0.15f);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
convertView = inflater.inflate(R.layout.item_layout, parent, false);
viewHolder=new ViewHolder();
viewHolder.seconds=(TextView) convertView.findViewById(R.id.recorder_time);
viewHolder.length=convertView.findViewById(R.id.recorder_length);
convertView.setTag(viewHolder);
}else {
viewHolder=(ViewHolder) convertView.getTag();
}
viewHolder.seconds.setText(Math.round(getItem(position).time)+"\"");
ViewGroup.LayoutParams lParams=viewHolder.length.getLayoutParams();
lParams.width=(int) (mMinItemWith+mMaxItemWith/60f*getItem(position).time);
viewHolder.length.setLayoutParams(lParams);
return convertView;
}
/**
* 定义一个ViewHolder
*/
class ViewHolder {
TextView seconds;// 时间
View length;// 对话框长度
}
}
如果大家感兴趣去搜索的话,会发现到现在为止代码和网上搜索的基本一样,那我们看了干嘛,别急,不是要有一个Main类吗?
如果大家再往上找到这样的一个防微信语音聊天的Demo 然后导入自己项目会发现报错:
查看后才发现是因为权限的问题,那我们就加上权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
再运行会发现还是报这个错误,为什么呢?
大家都知道Android6.0 以后,对于一些危险权限的添加需要去动态添加。这里“录音”,“SD卡”的权限都属于危险权限,所以我们要动态添加权限才可以,我们看下MainActivity。java类:
public class MainActivity extends BaseActivity {
AudioRecordButton button;
private ListView mlistview;
private ArrayAdapter<Recorder> mAdapter;
private View viewanim;
private List<Recorder> mDatas = new ArrayList<Recorder>();
@Override
public int getLayoutResId() {
return R.layout.activity_main;
}
@Override
protected void initView() {
mlistview = (ListView) findViewById(R.id.listview);
button = (AudioRecordButton) findViewById(R.id.recordButton);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//SDK>=23 编译版本 > 23
//checkSelfPermission用来检测应用是否已经具有权限
if (!(ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,Manifest.permission.RECORD_AUDIO)) {
//去设置权限(申请权限) dialog
MySystemUtils.goToSetPermission(activity, getResources().getString(R.string.permission_record_audio), JHConstants.RECORD_AUDIO);
} else {
//进行请求单个或多个权限
ActivityCompat.requestPermissions(activity,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, JHConstants.RECORD_AUDIO);
}
} else {
Log.i("--", "onClick granted");
button.setAudioFinishRecorderListener(new AudioRecordButton.AudioFinishRecorderListener() {
@Override
public void onFinished(float seconds, String filePath) {
// TODO Auto-generated method stub
Recorder recorder = new Recorder(seconds, filePath);
mDatas.add(recorder);
mAdapter.notifyDataSetChanged();
mlistview.setSelection(mDatas.size() - 1);
}
});
}
} else {
button.setAudioFinishRecorderListener(new AudioRecordButton.AudioFinishRecorderListener() {
@Override
public void onFinished(float seconds, String filePath) {
// TODO Auto-generated method stub
Recorder recorder = new Recorder(seconds, filePath);
mDatas.add(recorder);
mAdapter.notifyDataSetChanged();
mlistview.setSelection(mDatas.size() - 1);
}
});
}
}
@Override
protected void initListener() {
/* button.setAudioFinishRecorderListener(new AudioRecordButton.AudioFinishRecorderListener() {
@Override
public void onFinished(float seconds, String filePath) {
// TODO Auto-generated method stub
Recorder recorder = new Recorder(seconds, filePath);
mDatas.add(recorder);
mAdapter.notifyDataSetChanged();
mlistview.setSelection(mDatas.size() - 1);
}
});*/
mAdapter = new RecorderAdapter(this, mDatas);
mlistview.setAdapter(mAdapter);
mlistview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// TODO Auto-generated method stub
// 播放动画
if (viewanim != null) {//让第二个播放的时候第一个停止播放
viewanim.setBackgroundResource(R.drawable.adj);
viewanim = null;
}
viewanim = view.findViewById(R.id.id_recorder_anim);
viewanim.setBackgroundResource(R.drawable.play);
AnimationDrawable drawable = (AnimationDrawable) viewanim.getBackground();
drawable.start();
// 播放音频
MediaManager.playSound(mDatas.get(position).filePathString, new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
viewanim.setBackgroundResource(R.drawable.adj);
}
});
}
});
}
@Override
protected void initData() {
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
MediaManager.pause();
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
MediaManager.resume();
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
MediaManager.release();
}
public class Recorder {
float time;
String filePathString;
public Recorder(float time, String filePathString) {
super();
this.time = time;
this.filePathString = filePathString;
}
public float getTime() {
return time;
}
public void setTime(float time) {
this.time = time;
}
public String getFilePathString() {
return filePathString;
}
public void setFilePathString(String filePathString) {
this.filePathString = filePathString;
}
}
@Override//权限请求结果 用户对请求作出响应后的回调
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == JHConstants.RECORD_AUDIO) {
Boolean grantResultBoolean = false;
for (int grantResult : grantResults) {
grantResultBoolean = (grantResult == PackageManager.PERMISSION_GRANTED);
if (!grantResultBoolean) {
break;
}
}
if (grantResultBoolean) {
//通过
button.setAudioFinishRecorderListener(new AudioRecordButton.AudioFinishRecorderListener() {
@Override
public void onFinished(float seconds, String filePath) {
// TODO Auto-generated method stub
Recorder recorder = new Recorder(seconds, filePath);
mDatas.add(recorder);
mAdapter.notifyDataSetChanged();
mlistview.setSelection(mDatas.size() - 1);
}
});
} else {
MySystemUtils.goToSetPermission(activity, getResources().getString(R.string.permission_record_audio_and_sd), JHConstants.RECORD_AUDIO);
}
}
}
}
这样就可以正常运行了。
当然有的同志在测试的时候用的是模拟器:发现还是报错误
如果你去查找一下会发现:
好了,这里我就不多说什么了,记得要在真机上运行才可以。