[置顶] Android仿微信语音聊天
16lz
2021-01-26
1、项目界面展示:
2、项目代码整体结构简析:
(1)AudioRecorderButton类(录音按钮):State:STATE_NORMAL【正常状态】、STATE_RECORDERING【正在录音的状态】、STATE_WANT_TO_CALCEL【想要放弃录音的装填】;
(2)AudioDialogManage类(录音过程中的提示对话框):Style:RECORDERING【正在录音】、WANT_TO_CANCEL【取消提示对话框】、TOO_SHAORT【录音时间过短提醒对话框】;
(3)AudioManage类(控制录音):prepare()(end prepare-—》callback)【去录音】, cancel()【取消录音】, release()【正常结束】(-—》callbackToActivity), getVoiceLevel()【获得音量大小】;
伪码展示:
class AudioRecorderButton{onTouchEvent(){DOWN: // 按下changeButtonState(STATE_RECORDERING);onLongClick -> AudioManage.prepare() -> end prepare -> AudioDialogManage.showDialog(RECORDERING) MOVE: // 滑动if(wantCancel(x,y)){changeButtonState(STATE_WANT_TO_CALCEL); //更新ButtonAudioDialogManage.showDialog(WANT_TO_CANCEL); //更新Dialog}else{changeButtonState(STATE_RECORDERING);AudioDialogManage.showDialog(RECORDERING);}UP: // 抬起if(wantCancel == curState) //取消{AudioManage.cancel();}if(STATE_RECORDERING = curState) //正常结束{AudioManage.release();callbackToActivity(path,time);}......}}
1、主界面布局:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <!-- 语音列表 --> <ListView android:id="@+id/voiceNotesListView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#ebebeb" android:divider="@null" android:dividerHeight="10dp" > </ListView> <!-- 底部按钮布局 --> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <songshi.suishou.tool.AudioRecorderButton android:id="@+id/recorderButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:layout_marginRight="50dp" android:layout_marginTop="6dp" android:layout_marginBottom="7dp" android:gravity="center" android:padding="5dp" android:minHeight="0dp" android:background="@drawable/btn_recorder_normal" android:text="@string/str_recorder_normal" android:textColor="#727272" > </songshi.suishou.tool.AudioRecorderButton> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="#ccc"/> </FrameLayout></LinearLayout>
<string name="str_recorder_normal">按住 说话</string><string name="str_recorder_recording">松开 结束</string><string name="str_recorder_want_cancel">滑动手指,取消记录</string>
2、AudioRecorderButton类:
package com.example.voicenotes;import com.example.voicenotes.AudioManage.AudioStateListenter;import android.content.Context;import android.os.Environment;import android.os.Handler;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.Button;public class AudioRecorderButton extends Button implements AudioStateListenter {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; // 是否触发onLongClickprivate static final int DISTANCE_Y_CANCEL = 50;private AudioDialogManage audioDialogManage;private AudioManage mAudioManage;//构造方法public AudioRecorderButton(Context context) {super(context, null);// TODO Auto-generated constructor stub}public AudioRecorderButton(Context context, AttributeSet attrs) {super(context, attrs);audioDialogManage = new AudioDialogManage(getContext());String dir = Environment.getExternalStorageDirectory()+ "/VoiceRecorder";// 此处需要判断是否有存储卡mAudioManage = AudioManage.getInstance(dir);mAudioManage.setOnAudioStateListenter(this);setOnLongClickListener(new OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {// TODO Auto-generated method stubmReady = true;// 真正显示应该在audio end prepared以后mAudioManage.prepareAudio();//return true;return false;}});// TODO Auto-generated constructor stub}//录音完成后的回调public interface AudioFinishRecorderListenter{void onFinish(float seconds,String FilePath);}private AudioFinishRecorderListenter mListenter;public void setAudioFinishRecorderListenter(AudioFinishRecorderListenter listenter){this.mListenter=listenter;}//复写onTouchEvent@Overridepublic boolean onTouchEvent(MotionEvent event) {int action = event.getAction(); //获取当前Actionint 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) { //没有触发onLongClickreset();return super.onTouchEvent(event);}if (!isRecordering || mTime < 0.5f) {audioDialogManage.tooShort();mAudioManage.cancel();mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DIMISS, 1300);// 延迟,1.3秒以后关闭时间过短对话框} /*else if (mTime < 0.5f) {audioDialogManage.tooShort();isRecordering=false;mAudioManage.cancel();mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DIMISS, 1300);// 延迟}*/else if (mCurState == STATE_RECORDERING) { //正常录制结束audioDialogManage.dimissDialog();// releasemAudioManage.release();// callbackToActif(mListenter!=null){mListenter.onFinish(mTime, mAudioManage.getCurrentFilePath());}} else if (mCurState == STATE_WANT_TO_CALCEL) {// cancelaudioDialogManage.dimissDialog();mAudioManage.cancel();}reset();break;}return super.onTouchEvent(event);}// 恢复状态以及一些标志位private void reset() {isRecordering = false;mReady = false;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的背景和文本private void changeState(int state) {if (mCurState != state) {mCurState = state;switch (state) {case STATE_NORMAL:setBackgroundResource(R.drawable.btn_recorder_normal);setText(R.string.str_recorder_normal);break;case STATE_RECORDERING:setBackgroundResource(R.drawable.btn_recorder_recordering);setText(R.string.str_recorder_recording);if (isRecordering) {// 更新Dialog.recording()audioDialogManage.recording();}break;case STATE_WANT_TO_CALCEL:setBackgroundResource(R.drawable.btn_recorder_recordering);setText(R.string.str_recorder_want_cancel);// 更新Dialog.wantCancel()audioDialogManage.wantToCancel();break;}}}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 float mTime;// 获取音量大小的Runnableprivate Runnable mGetVoiceLevelRunnable = new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubwhile (isRecordering) {try {Thread.sleep(100);mTime += 0.1f;mHandler.sendEmptyMessage(MSG_VOICE_CHANGE);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}};private Handler mHandler = new Handler() {public void handleMessage(android.os.Message msg) {switch (msg.what) {case MSG_AUDIO_PREPARED:audioDialogManage.showRecorderingDialog();isRecordering = true;new Thread(mGetVoiceLevelRunnable).start();break;case MSG_VOICE_CHANGE:audioDialogManage.updateVoiceLevel(mAudioManage.getVoiceLevel(7));break;case MSG_DIALOG_DIMISS:audioDialogManage.dimissDialog();break;}};};@Overridepublic void wellPrepared() {// TODO Auto-generated method stubmHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);}}
3、AudioDialogManage类:
Dialog布局:
<?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:background="@drawable/dialog_loading_bg" android:gravity="center" android:orientation="vertical" android:padding="20dp" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:id="@+id/recorder_dialog_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/recorder" android:visibility="visible" /> <ImageView android:id="@+id/recorder_dialog_voice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/v1" android:visibility="visible" /> </LinearLayout> <TextView android:id="@+id/recorder_dialog_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:text="手指滑动,取消录音" android:textColor="#FFFFFFFF" /></LinearLayout>AudioDialogManage类:
package com.example.voicenotes;import android.app.Dialog;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.widget.ImageView;import android.widget.TextView;public class AudioDialogManage {private Dialog mDialog;private ImageView mIcon;private ImageView mVoice;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.tools_voice_notes_dialog_recorder, null);mDialog.setContentView(view);mIcon = (ImageView) mDialog.findViewById(R.id.recorder_dialog_icon);mVoice = (ImageView) mDialog.findViewById(R.id.recorder_dialog_voice);mLabel = (TextView) mDialog.findViewById(R.id.recorder_dialog_label);mDialog.show();}//正在录音时,Dialog的显示public void recording() {if (mDialog != null && mDialog.isShowing()) {mIcon.setVisibility(View.VISIBLE);mVoice.setVisibility(View.VISIBLE);mLabel.setVisibility(View.VISIBLE);mIcon.setImageResource(R.drawable.recorder);mLabel.setText("手指滑动,取消录音");}}public void wantToCancel() {if (mDialog != null && mDialog.isShowing()) {mIcon.setVisibility(View.VISIBLE);mVoice.setVisibility(View.GONE);mLabel.setVisibility(View.VISIBLE);mIcon.setImageResource(R.drawable.cancel);mLabel.setText("松开手指,取消录音");}}//录音时间过短public void tooShort() {if (mDialog != null && mDialog.isShowing()) {mIcon.setVisibility(View.VISIBLE);mVoice.setVisibility(View.GONE);mLabel.setVisibility(View.VISIBLE);mIcon.setImageResource(R.drawable.voice_to_short);mLabel.setText("录音时间过短");}}public void dimissDialog() {if (mDialog != null && mDialog.isShowing()) {mDialog.dismiss();mDialog = null;}}//通过Level更新Voice的图片V1——V7public void updateVoiceLevel(int level) {if (mDialog != null && mDialog.isShowing()) {//mIcon.setVisibility(View.VISIBLE);//mVoice.setVisibility(View.VISIBLE);//mLabel.setVisibility(View.VISIBLE);int resId=mContext.getResources().getIdentifier("v"+level, "drawable", mContext.getPackageName());mVoice.setImageResource(resId);}}}
4、AudioManage类:
MediaRecorder:http://developer.android.com/reference/android/media/MediaRecorder.html
MediaRecorder状态转换图:
AudioManage类:
package com.example.voicenotes;import java.io.File;import java.io.IOException;import java.util.UUID;import android.media.MediaRecorder;public class AudioManage {private MediaRecorder mMediaRecorder;private String mDir; // 文件夹的名称private String mCurrentFilePath;private static AudioManage mInstance;private boolean isPrepared; // 标识MediaRecorder准备完毕private AudioManage(String dir) {mDir = dir;}// 回调准备完毕public interface AudioStateListenter {void wellPrepared(); // prepared完毕}public AudioStateListenter mListenter;public void setOnAudioStateListenter(AudioStateListenter audioStateListenter) {mListenter = audioStateListenter;}// 单例实现 AudioManage//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); // 路径+文件名字mCurrentFilePath = file.getAbsolutePath();mMediaRecorder = new MediaRecorder();mMediaRecorder.setOutputFile(file.getAbsolutePath());// 设置输出文件mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置MediaRecorder的音频源为麦克风mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);// 设置音频的格式mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);// 设置音频的编码为AMR_NBmMediaRecorder.prepare();mMediaRecorder.start();isPrepared = true; // 准备结束if (mListenter != null) {mListenter.wellPrepared();}} catch (IllegalStateException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}// 随机生成文件名称private String GenerateFileName() {// TODO Auto-generated method stubreturn UUID.randomUUID().toString() + ".amr"; // 音频文件格式}// 获得音量等级public int getVoiceLevel(int maxLevel) {if (isPrepared) {try {return maxLevel * mMediaRecorder.getMaxAmplitude() / 32768 + 1;} catch (Exception e) {// TODO Auto-generated catch block// e.printStackTrace();}}return 1;}// 释放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 stubreturn mCurrentFilePath;}}
5、MediaManage类:主要管理语音的播放、暂停、恢复、停止
package com.example.voicenotes;import java.io.IOException;import android.media.AudioManager;import android.media.MediaPlayer;import android.media.MediaPlayer.OnErrorListener;public class MediaManage {private static MediaPlayer mMediaPlayer;private static boolean isPause;public static void playSound(String filePath,MediaPlayer.OnCompletionListener onCompletionListenter){if(mMediaPlayer==null){mMediaPlayer=new MediaPlayer();mMediaPlayer.setOnErrorListener( new OnErrorListener() {@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {// TODO Auto-generated method stubmMediaPlayer.reset();return false;}});}else{mMediaPlayer.reset();}try {mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mMediaPlayer.setOnCompletionListener(onCompletionListenter);mMediaPlayer.setDataSource(filePath);mMediaPlayer.prepare();mMediaPlayer.start();} catch (IllegalArgumentException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (SecurityException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalStateException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.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;}}}
6、MainActivity:
ListView布局:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="60dp" android:layout_marginTop="5dp" > <ImageView android:id="@+id/voice_robot" android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:src="@drawable/robot" /> <FrameLayout android:id="@+id/recorder_length" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@id/voice_robot" android:background="@drawable/popup_inline_error" > <View android:id="@+id/recorder_anim" android:layout_width="25dp" android:layout_height="25dp" android:layout_gravity="center_vertical|left" android:background="@drawable/presence_audio_online"/> </FrameLayout> <TextView android:id="@+id/recorder_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@id/recorder_length" android:text="" android:layout_marginLeft="3dp" android:textColor="#ff777777"/></RelativeLayout>AudioRecorderAdapter:
package com.example.voicenotes;import java.util.List;import android.content.Context;import android.util.DisplayMetrics;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.view.WindowManager;import android.widget.ArrayAdapter;import android.widget.TextView;import com.example.voicenotes.MainActivity.Recorder;public class AudioRecorderAdapter extends ArrayAdapter<Recorder> { private List<Recorder> mDatas;<span style="white-space:pre"></span>private Context mContext;private int mMinItemWidth;private int mMaxItemWidth;private LayoutInflater mInflater;public AudioRecorderAdapter(Context context, List<Recorder> datas) {super(context, -1, datas);<span style="white-space:pre"></span>mContext=context;<span style="white-space:pre"></span>mDatas=datas;<span style="white-space:pre"></span><span style="white-space:pre"></span>mInflater=LayoutInflater.from(context);WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics outMetrics = new DisplayMetrics();windowManager.getDefaultDisplay().getMetrics(outMetrics);mMaxItemWidth = (int) (outMetrics.widthPixels * 0.7f);mMinItemWidth = (int) (outMetrics.widthPixels * 0.15f);}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {// TODO Auto-generated method stubViewHolder holder = null;if (convertView == null) {convertView = mInflater.inflate(R.layout.tools_voice_notes_recorder_item, parent, false);holder=new ViewHolder();holder.seconds=(TextView) convertView.findViewById(R.id.recorder_time);holder.mLength=convertView.findViewById(R.id.recorder_length);convertView.setTag(holder);}else{holder=(ViewHolder) convertView.getTag();}holder.seconds.setText(Math.round(getItem(position).getTime())+"\"");ViewGroup.LayoutParams lp=holder.mLength.getLayoutParams();lp.width=(int) (mMinItemWidth+(mMaxItemWidth/60f * getItem(position).getTime()));return convertView;}private class ViewHolder {TextView seconds;View mLength;}}MainActivity:
package com.example.voicenotes;import java.util.ArrayList;import java.util.List;import android.app.Activity;import android.graphics.drawable.AnimationDrawable;import android.media.MediaPlayer;import android.os.Bundle;import android.view.View;import android.widget.AdapterView;import android.widget.AdapterView.OnItemClickListener;import android.widget.ArrayAdapter;import android.widget.ListView;import com.example.voicenotes.AudioRecorderButton.AudioFinishRecorderListenter;public class MainActivity extends Activity {private ListView mListView;private ArrayAdapter<Recorder> mAdapter;private List<Recorder> mDatas = new ArrayList<Recorder>();private AudioRecorderButton mAudioRecorderButton;private View mAnimView;@Overrideprotected void onCreate(Bundle savedInstanceState) {// TODO Auto-generated method stubsuper.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mListView = (ListView) findViewById(R.id.voiceNotesListView);mAudioRecorderButton = (AudioRecorderButton) findViewById(R.id.recorderButton);mAudioRecorderButton.setAudioFinishRecorderListenter(new AudioFinishRecorderListenter() {@Overridepublic void onFinish(float seconds, String FilePath) {// TODO Auto-generated method stubRecorder recorder = new Recorder(seconds, FilePath);mDatas.add(recorder);mAdapter.notifyDataSetChanged();mListView.setSelection(mDatas.size() - 1);}});mAdapter = new AudioRecorderAdapter(this, mDatas);mListView.setAdapter(mAdapter);mListView.setOnItemClickListener(new OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view,int position, long id) {if (mAnimView != null) {mAnimView.setBackgroundResource(R.drawable.adj);mAnimView = null;}// 播放动画mAnimView = view.findViewById(R.id.recorder_anim);mAnimView.setBackgroundResource(R.drawable.audio_play_anim);AnimationDrawable anim = (AnimationDrawable) mAnimView.getBackground();anim.start();// 播放音频MediaManage.playSound(mDatas.get(position).filePath,new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {mAnimView.setBackgroundResource(R.drawable.adj);}});}});}public class Recorder {float time;String filePath;public Recorder(float time, String filePath) {super();this.time = time;this.filePath = filePath;}public float getTime() {return time;}public void setTime(float time) {this.time = time;}public String getFilePath() {return filePath;}public void setFilePath(String filePath) {this.filePath = filePath;}}@Overrideprotected void onPause() {// TODO Auto-generated method stubsuper.onPause();MediaManage.pause();}@Overrideprotected void onResume() {// TODO Auto-generated method stubsuper.onResume();MediaManage.resume();}@Overrideprotected void onDestroy() {// TODO Auto-generated method stubsuper.onDestroy();MediaManage.release();}}全部代码:https://github.com/songshimvp/VoiceNotes
更多相关文章
- Android(安卓)Fragment 生命周期图
- Android学习之路——Activity(1)
- android学习笔记NO.3
- Android(安卓)第三天(下午)
- Android(安卓)Audio简述
- 修改状态栏(StatusBar)图标(icon)(定制自己的状态栏)
- android状态栏一体化(改变状态栏的背景颜色)
- 关于Android主题splash全屏和主页面的沉浸式状态栏
- TextInputEditText样式设置