如上图,是常见的仿微信的聊天程序,实现的效果如上图所示,由于项目太大,本文只讲录音部分。本项目示例代码:https://github.com/xiangzhihong/weixinAudio
主要用到4个核心类:
自定义录音按钮(RecoderButton);
弹框管理类(RecorderDialog);
录音管理类(AudioManager);
录音播放类(MediaManager)。
其中
1.AudioRecordButton状态:
1.STATE_NORMAL:普通状态
2.STATE_RECORDING:录音中
3.STATE_CANCEL:取消录音
2.DialogManager状态:
1.RECORDING:录音中
2.WANT_TO_CANCEL:取消录音
3.TOO_SHORT:录音时间太短
3.AudioManager:
1.prepare():准备状态
2.cancel():取消录音
3.release():正常结束录音
4.getVoiceLevel():获取音量
代码实现
自定义Button,重写onTouchEvent()方法,用于执行长按录音操作。
class AudioRecorderButton{
onTouchEvent(){
DOWN:
changeButtonState(STATE_RECORDING);
| DialogManager.showDialog(RECORDING)
触发LongClick事件(AudioManager.prepare() --> end prepared --> | );
| getVoiceLevel();//开启一个线程,更新Dialog上的音量等级
MOVE:
if(wantCancel(x,y)){
DialogManager.showDialog(WANT_TO_CANCEL);更新Dialog
changeButtonState(STATE_WANT_TO_CANCEL);更新Button状态
}else{
DialogManager.showDialog(WANT_TO_CANCEL);
changeButtonState(STATE_RECORDING);
}
UP:
if(wantCancel == curState){//当前状态是想取消状态
AudioManager.cancel();
}
if(STATE_RECORDING = curState){
if(tooShort){//判断录制时长,如果录制时间过短
DialogManager.showDialog(TOO_SHORT);
}
AudioManager.release();
callbackActivity(url,time);//(当前录音文件路径,时长)
}
}
}
相关的逻辑请查看项目源码。
MediaManager
public class MediaManager {
private static MediaPlayer mMediaPlayer;
private static boolean isPause;
public static void playSound(String filePath,
MediaPlayer.OnCompletionListener onCompletionListener) {
if(mMediaPlayer == null){
mMediaPlayer = new MediaPlayer();
}else {
mMediaPlayer.reset();
}
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnCompletionListener(onCompletionListener);
try {
mMediaPlayer.setDataSource(filePath);
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void pause(){
if(mMediaPlayer != null && mMediaPlayer.isPlaying()){
mMediaPlayer.pause();
isPause = true;
}
}
public static void resume(){
if(mMediaPlayer != null && isPause){
mMediaPlayer.start();
isPause = false;
}
}
public static void release(){
if(mMediaPlayer != null){
mMediaPlayer.release();
mMediaPlayer = null;
}
}
}
RecorderDialog
录音弹窗类,主要包含录音的各种状态及弹窗。源码如下:
public class RecorderDialog {
private Dialog mDialog;
private ImageView mIcon;
private TextView mLable;
private TextView mLeftTime;
private Context mContext;
public RecorderDialog(Context context) {
this.mContext = context;
}
public void showRecordingDialog() {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.dialog_recorder, null);
mDialog = new Dialog(mContext, R.style.style_dialog);
mDialog.setContentView(view);
mIcon = (ImageView) mDialog.findViewById(R.id.recorder_dialog_icon);
mLable = (TextView) mDialog.findViewById(R.id.recoder_dialog_label);
mLeftTime=(TextView) mDialog.findViewById(R.id.recoder_leftTime);
mDialog.show();
}
public void recording() {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setImageResource(R.mipmap.recorder_icon);
mLable.setText("手指上滑,取消发送");
}
}
public void wantToCancel() {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setImageResource(R.mipmap.cancel_recorder_icon);
mLable.setText("松开手指,取消发送");
}
}
public void tooShort() {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setImageResource(R.mipmap.voice_to_short);
mLable.setText("录音时间过短");
}
}
//倒计时提示(10-->0)
public void recoderConfirm(int time) {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setVisibility(View.GONE);
mLeftTime.setVisibility(View.VISIBLE);
mLeftTime.setText(time+"");
mLable.setText("松开手指,取消发送");
}
}
public void dimissDialog() {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
mDialog = null;
}
}
public void setVoiceLevel(int level) {
if (mDialog != null && mDialog.isShowing()) {
//用switch冗余
int resId = mContext.getResources().getIdentifier("v"+level,"mipmap",mContext.getPackageName());
}
}
}
由于需要用到权限系统,所以需要在配置文件中添加相关的权限。
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
RecoderButton
自定义录音按钮,录音的一切判断都在这个文件。
public class RecoderButton extends TextView implements AudioManager.AudioStateListener {
private static final int DISANCE_Y_CANCEL = 50;
private static final int STATE_RECORDER_NORMAL = 1; //正常
private static final int STATE_RECORDING = 2; //正在录制
private static final int STATE_CANCEL = 3; //取消
private static final int MSG_AUDIO_PREPARED = 0X10;
private static final int MSG_AUDIO_CHANGED = 0X11;
private static final int MSG_AUDIO_DIMISS = 0X12;
private static final int MSG_AUDIO_TIME_OUT = 0X13;
private boolean mReady;
private int mCurState = STATE_RECORDER_NORMAL;
private boolean isRecording = false;//正在录音
private float maxTime=10;//最大录制时长
private float mTime=0;//录制时长
private Timer timer = new Timer();
private int leftTime=10;//录音倒计时,10开始提示
private RecorderDialog dialog=null;
private AudioManager audioManager=null;
private FinishRecorderListener mListener;
public RecoderButton(Context context) {
this(context, null);
}
public RecoderButton(Context context, AttributeSet attrs) {
super(context, attrs);
initDialog();
initClick();
}
private void initClick() {
setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mReady = true;
audioManager.prepareAudio();
return false;
}
});
}
private void initDialog() {
dialog = new RecorderDialog(getContext());
String dir = Environment.getExternalStorageDirectory() + "/recorder";//创建文件夹
audioManager = AudioManager.getInstance(dir);
audioManager.setOnAudioStateListener(this);
}
/**
* 获取音量大小
*/
private Runnable mGetVoiceLeveelRunnable = new Runnable() {
@Override
public void run() {
while (isRecording) {
try {
Thread.sleep(100);
mTime += 0.1f;
mHandler.sendEmptyMessage(MSG_AUDIO_CHANGED);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_AUDIO_PREPARED:
dialog.showDialog();
isRecording = true;
new Thread(mGetVoiceLeveelRunnable).start();
break;
case MSG_AUDIO_CHANGED:
dialog.voiceLevel(audioManager.getVoiceLevel(7));
System.out.println("录音时间:"+mTime);
if (mTime>maxTime){
confirmTimer();
}
break;
case MSG_AUDIO_DIMISS:
dialog.dimissDialog();
break;
}
}
};
//录音倒计时
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
leftTime--;
if (leftTime<=0){
dialog.dimissDialog();
audioManager.release();
if (mListener != null) {
mListener.onFinish(mTime, audioManager.getCurrentFilePath());
}
return;
}
dialog.recoderConfirm(leftTime);
}
};
@Override
public void wellPrepared() {
mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
}
@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:
changeState(STATE_RECORDING);
break;
case MotionEvent.ACTION_MOVE:
if (isRecording) {
//根据x y的坐标判断是否想取消
if (wantToCancel(x, y)) {
changeState(STATE_CANCEL);
} else {
changeState(STATE_RECORDING);
}
}
break;
case MotionEvent.ACTION_UP:
if (!mReady) {
reset();
return super.onTouchEvent(event);
}
if (!isRecording || mTime < 1.0f) {
System.out.println("录音时间过短");
dialog.recoderShort();
audioManager.cancel();
mHandler.sendEmptyMessageDelayed(MSG_AUDIO_DIMISS, 1300);
} else if (mCurState == STATE_RECORDING) {
System.out.println("正常录制");
dialog.dimissDialog();
audioManager.release();
if (mListener != null) {
mListener.onFinish(mTime, audioManager.getCurrentFilePath());
}
} else if (mCurState == STATE_CANCEL) {
System.out.println("取消了");
dialog.dimissDialog();
audioManager.cancel();
}
reset();
break;
}
return super.onTouchEvent(event);
}
//恢复状态及标志位
private void reset() {
isRecording = false;
mReady = false;
mTime = 0;
changeState(STATE_RECORDER_NORMAL);
}
private boolean wantToCancel(int x, int y) {
if (x < 0 || x > getWidth()) {//判断手指的横坐标是否超出按钮的范围
return true;
}
//再判断Y
if (y < -DISANCE_Y_CANCEL || y > getHeight() + DISANCE_Y_CANCEL) {//按钮上部或下部
return true;
}
return false;
}
//随着状态的改变,文本颜色和背景改变
private void changeState(int state) {
if (mCurState != state) {
mCurState = state;
switch (state) {
case STATE_RECORDER_NORMAL:
setBackgroundResource(R.drawable.recentgle_gray_border);
setText("按住 说话");
break;
case STATE_RECORDING:
setBackgroundResource(R.drawable.recentgle_gray);
setText("松开结束");
if (isRecording) {
dialog.recording();
}
break;
case STATE_CANCEL:
setBackgroundResource(R.drawable.recentgle_gray);
setText("松开手指,取消发送");
dialog.cancelRecorder();
break;
}
}
}
//倒计时定时器
private void confirmTimer() {
timer.schedule(new TimerTask() {
@Override public void run() {
try {
Thread.sleep(1000);
handler.sendEmptyMessage(MSG_AUDIO_TIME_OUT);
}catch (Exception e){
e.printStackTrace();
}
}
}, 0, 1000);
}
public interface FinishRecorderListener {
void onFinish(float seconds, String filePath);
}
public void setRecorderListener(FinishRecorderListener listener) {
mListener = listener;
}
}
最后录制完成后,点击列表的语音会完成播放功能。
MediaManager
public class MediaManager {
public static MediaManager instance=null;
private MediaPlayer mMediaPlayer=null;
private static boolean isPause=false;
public static synchronized MediaManager getInstance() {
if (instance == null) {
instance = new MediaManager();
}
return instance;
}
public void playSound(String filePath){
playSound(filePath,null);
}
public void playSound(String filePath,
MediaPlayer.OnCompletionListener onCompletionListener) {
if(mMediaPlayer == null){
mMediaPlayer = new MediaPlayer();
}else {
mMediaPlayer.reset();
}
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnCompletionListener(onCompletionListener);
try {
mMediaPlayer.setDataSource(filePath);
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public void pause(){
if(mMediaPlayer != null && mMediaPlayer.isPlaying()){
mMediaPlayer.pause();
isPause = true;
}
}
public void resume(){
if(mMediaPlayer != null && isPause){
mMediaPlayer.start();
isPause = false;
}
}
public void release(){
if(mMediaPlayer != null){
mMediaPlayer.release();
mMediaPlayer = null;
}
}
}
对于聊天列表,是一个比较复杂的逻辑,开发的时候可以重写getItemViewType函数,然后不同的ViewType加载不同的视图,例如我的项目代码如下:
ChatItem struct = getItem(position);
switch (struct.chatType) {
case ChatItem.CHAT_TYPE_TIME:
return initTimeView(convertView, parent, (String) struct.data);
case ChatItem.CHAT_TYPE_GROUP_TIP:
return initTipView(convertView, parent, (String) struct.data);
case ChatItem.CHAT_TYPE_OUT_TEXT:
return initOutTextView(convertView, parent, (Message) struct.data);
case ChatItem.CHAT_TYPE_IN_TEXT:
return initInTextView(convertView, parent, (Message) struct.data);
case ChatItem.CHAT_TYPE_OUT_IMAGE:
return initOutImageView(convertView, parent, (Message) struct.data);
case ChatItem.CHAT_TYPE_IN_IMAGE:
return initInImageView(convertView, parent, (Message) struct.data);
case ChatItem.CHAT_TYPE_OUT_AUDIO:
return initOutAudioView(convertView, parent, (Message) struct.data);
case ChatItem.CHAT_TYPE_BONUS_NOTICE:
return initBonusNotice(convertView, parent, (Message) struct.data);
case ChatItem.CHAT_TYPE_RECOMMEND:
return initRecommendType(convertView, parent, (Message) struct.data);
default:
return new View(context);
}
如果,你想了解mp3格式相关的内容,可以查看下面的链接:http://blog.csdn.net/omrapollo/article/details/50470659
本文同步分享在 博客“xiangzhihong8”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/241554/blog/4824591