在此感谢鸿洋大神,因为我这是在慕课上看大神的视频做出来的。
代码中我已经添加了很多很多注释,不光是为了大家,也是为了自己能够更加透彻的理解该功能
支持原创,也不算原创了哈哈~
http://blog.csdn.net/lhk147852369/article/details/78658055
注意注意:
Android 6.0动态获取录音权限,我并没有加上,所以你们需要在写完代码后,运行时在权限管理中指定该权限
否则会崩溃哦~~, 当然你们可以改变as中的targerversion<23就可以了
话不多说,直接上效果图:
MainActivity :
public class MainActivity extends AppCompatActivity {private ListView mListView;private ArrayAdapter<Recorder> mAdapter;private List<Recorder> mDatas =new ArrayList<>();private AudioRecorderButton mAudioRecorderButton;private View mAnimView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();setListViewAdapter();}private void initView(){mListView = findViewById(R.id.id_listview);mAudioRecorderButton = findViewById(R.id.id_recorder_button);mAudioRecorderButton.setAudioFinishRecorderListener(new AudioRecorderButton.AudioFinishRecorderListener() {@Overridepublic void onFinish(float seconds, String filePath) {//每完成一次录音Recorder recorder = new Recorder(seconds,filePath);mDatas.add(recorder);//更新adaptermAdapter.notifyDataSetChanged();//设置listview 位置mListView.setSelection(mDatas.size()-1);}});}private void setListViewAdapter(){mAdapter = new RecorderAdapter(this, mDatas);mListView.setAdapter(mAdapter);mListView.setOnItemClickListener(new AdapterView.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.id_recorder_anim);mAnimView.setBackgroundResource(R.drawable.play_anim);AnimationDrawable animation = (AnimationDrawable) mAnimView.getBackground();animation.start();//播放音频 完成后改回原来的backgroundMediaManager.playSound(mDatas.get(position).filePath, new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {mAnimView.setBackgroundResource(R.drawable.adj);}});}});}/*** 根据生命周期 管理播放录音*/@Overrideprotected void onPause() {super.onPause();MediaManager.pause();}@Overrideprotected void onResume() {super.onResume();MediaManager.resume();}@Overrideprotected void onDestroy() {super.onDestroy();MediaManager.release();}//数据类class Recorder{float time;String 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;}public Recorder(float time, String filePath) {super();this.time = time;this.filePath = filePath;}}
}
主页面:
需要注意:改下自定义button的包名哦,不然会找不到的
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.lgoutech.weixin_recorder.MainActivity"><ListViewandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:id="@+id/id_listview"android:background="#ebebeb"android:divider="@null"android:dividerHeight="10dp"></ListView><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><Viewandroid:layout_width="wrap_content"android:layout_height="1dp"android:background="#ccc"/><com.lgoutech.weixin_recorder.view.AudioRecorderButtonandroid:id="@+id/id_recorder_button"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="50dp"android:layout_marginRight="50dp"android:gravity="center"android:layout_marginTop="7dp"android:layout_marginBottom="7dp"android:padding="5dp"android:text="@string/str_recorder_normal"android:textColor="#727272"android:background="@drawable/btn_recorder_normal"android:minHeight="0dp"></com.lgoutech.weixin_recorder.view.AudioRecorderButton></LinearLayout>
</LinearLayout>
自定义的dialog、xml文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"android:padding="20dp"android:gravity="center"android:background="@drawable/dialog_loading_bg"tools:context="com.lgoutech.weixin_recorder.MainActivity"><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="horizontal"><ImageViewandroid:id="@+id/id_recorder_dialog_icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/recorder"android:visibility="visible"/><ImageViewandroid:id="@+id/id_recorder_dialog_voice"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/v1"android:visibility="visible"/></LinearLayout><TextViewandroid:id="@+id/id_recorder_dialog_label"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:text="手指上划,取消发送"android:textColor="#FFFFFF"/>
</LinearLayout>
ListView的Item布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="60dp"android:layout_marginTop="5dp"tools:context="com.lgoutech.weixin_recorder.MainActivity"><ImageViewandroid:id="@+id/id_icon"android:layout_width="40dp"android:layout_height="40dp"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:layout_marginRight="5dp"android:src="@drawable/icon"/><LinearLayoutandroid:id="@+id/id_recorder_length"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerVertical="true"android:orientation="vertical"android:layout_toLeftOf="@id/id_icon"android:background="@drawable/chatto_bg_focused"><Viewandroid:id="@+id/id_recorder_anim"android:layout_width="25dp"android:layout_height="25dp"android:layout_gravity="center_vertical|right"android:background="@drawable/adj"/></LinearLayout><TextViewandroid:id="@+id/id_recorder_time"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_toLeftOf="@id/id_recorder_length"android:layout_marginRight="3dp"android:textColor="#ff777777"android:layout_centerVertical="true"/></RelativeLayout>
录音管理类:
public class AudioManager {private MediaRecorder mMediaRecorder;private String mDir;private String mCurrentFilePath;private static AudioManager mInstance;private boolean isPrepared;public AudioManager(String dir){mDir = dir;};/*** 回调准备完毕*/public interface AudioStateListener {void wellPrepared();}public AudioStateListener mListener;public void setOnAudioStateListener(AudioStateListener listener){mListener = listener;}public static AudioManager getInstance(String dir){if (mInstance == null) {synchronized (AudioManager.class) {if (mInstance == null) {mInstance = new AudioManager(dir);}}}return mInstance;}/*** 准备*/public void prepareAudio() {try {isPrepared = false;File dir = new File(mDir);if (!dir.exists()) {dir.mkdir();}String fileName = generateFileName();File file = new File(dir, fileName);mCurrentFilePath = file.getAbsolutePath();mMediaRecorder = new MediaRecorder();//设置输出文件mMediaRecorder.setOutputFile(file.getAbsolutePath());//设置MediaRecorder的音频源为麦克风mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//设置音频格式mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);//设置音频的格式为amrmMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);mMediaRecorder.prepare();mMediaRecorder.start();//准备结束isPrepared = true;if (mListener != null) {mListener.wellPrepared();}} catch (IOException e) {e.printStackTrace();}}// 生成UUID唯一标示符
// 算法的核心思想是结合机器的网卡、当地时间、一个随即数来生成GUID
// .amr音频文件private String generateFileName() {return UUID.randomUUID().toString()+".amr";}public int getVoiceLevel(int maxLevel) {if (isPrepared) {//获得最大的振幅getMaxAmplitude() 1-32767try {return maxLevel * mMediaRecorder.getMaxAmplitude()/32768+1;} catch (Exception e) {}}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() {return mCurrentFilePath;}
}
最重要的自定义按钮,很多逻辑都在这里:
/*** 自定义按钮 实现录音等功能* Created by Administrator on 2017/11/28.*/@SuppressLint("AppCompatCustomView")
public class AudioRecorderButton extends Button implements AudioManager.AudioStateListener {//手指滑动 距离private static final int DISTANCE_Y_CANCEL = 50;//状态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 = false;private DialogManager mDialogManager;private AudioManager mAudioManager;private float mTime;//是否触发onlongclickprivate boolean mReady;public AudioRecorderButton(Context context) {this(context, null);}public AudioRecorderButton(Context context, AttributeSet attrs) {super(context, attrs);mDialogManager = new DialogManager(getContext());//偷个懒,并没有判断 是否存在, 是否可读。String dir = Environment.getExternalStorageDirectory() + "/recorder_audios";mAudioManager = new AudioManager(dir);mAudioManager.setOnAudioStateListener(this);//按钮长按 准备录音 包括startsetOnLongClickListener(new OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {mReady = true;mAudioManager.prepareAudio();return false;}});}/*** 录音完成后的回调*/public interface AudioFinishRecorderListener{//时长 和 文件void onFinish(float seconds,String filePath);}private AudioFinishRecorderListener mListener;public void setAudioFinishRecorderListener (AudioFinishRecorderListener listener){mListener = listener;}//获取音量大小的Runnableprivate Runnable mGetVoiceLevelRunnable = new Runnable() {@Overridepublic void run() {while (isRecording) {try {Thread.sleep(100);mTime += 0.1;mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);} catch (InterruptedException e) {e.printStackTrace();}}}};private static final int MSG_AUDIO_PREPARED = 0X110;private static final int MSG_VOICE_CHANGED = 0X111;private static final int MSG_DIALOG_DIMISS = 0X112;private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case MSG_AUDIO_PREPARED ://TODO 真正现实应该在audio end prepared以后mDialogManager.showRecordingDialog();isRecording = true;new Thread(mGetVoiceLevelRunnable).start();break;case MSG_VOICE_CHANGED :mDialogManager.updateVoiceLevel(mAudioManager.getVoiceLevel(7));break;case MSG_DIALOG_DIMISS :mDialogManager.dimissDialog();break;}}};@Overridepublic void wellPrepared() {mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);}@Overridepublic boolean onTouchEvent(MotionEvent event) {int action = event.getAction();int x = (int) event.getX();int y = (int) event.getY();switch (action) {case MotionEvent.ACTION_DOWN://TODOisRecording = true;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://如果longclick 没触发if (!mReady) {reset();return super.onTouchEvent(event);}//触发了onlongclick 没准备好,但是已经prepared 已经start//所以消除文件夹if(!isRecording||mTime<0.6f){mDialogManager.tooShort();mAudioManager.cancel();mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DIMISS, 1300);}else if(mCurState==STATE_RECORDING){//正常录制结束mDialogManager.dimissDialog();mAudioManager.release();if (mListener != null) {mListener.onFinish(mTime,mAudioManager.getCurrentFilePath());}}else if (mCurState == STATE_RECORDING) {mDialogManager.dimissDialog();//release//callbacktoAct} else if (mCurState == STATE_WANT_TO_CANCEL) {mDialogManager.dimissDialog();mAudioManager.cancel();//cancel}reset();break;}return super.onTouchEvent(event);}/*** 恢复状态 标志位*/private void reset() {isRecording = false;mReady = false;changeState(STATE_NORMAL);mTime = 0;}private boolean wantToCancel(int x, int y) {//如果左右滑出 buttonif (x < 0 || x > getWidth()) {return true;}//如果上下滑出 button 加上我们自定义的距离if (y < -DISTANCE_Y_CANCEL || y > getHeight() + DISTANCE_Y_CANCEL) {return true;}return false;}//改变状态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_RECORDING:setBackgroundResource(R.drawable.btn_recording);setText(R.string.str_recorder_recording);if (isRecording) {mDialogManager.recording();}break;case STATE_WANT_TO_CANCEL:setBackgroundResource(R.drawable.btn_recording);setText(R.string.str_recorder_want_cancel);mDialogManager.wantToCancel();break;}}}}
Dialog管理类:
public class DialogManager {private Dialog mDialog;private ImageView mIcon;private ImageView mVoice;private TextView mLable;private Context mContext;public DialogManager(Context context){mContext = context;}public void showRecordingDialog(){mDialog = new Dialog(mContext, R.style.Theme_AudioDialog);LayoutInflater inflater = LayoutInflater.from(mContext);View view=inflater.inflate(R.layout.dialog_recorder,null);mDialog.setContentView(view);mIcon = view.findViewById(R.id.id_recorder_dialog_icon);mVoice = view.findViewById(R.id.id_recorder_dialog_voice);mLable = view.findViewById(R.id.id_recorder_dialog_label);mDialog.show();}//正在播放时的状态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("手指上划,取消发送");}}//想要取消public void wantToCancel(){if (mDialog != null&&mDialog.isShowing()) {mIcon.setVisibility(View.VISIBLE);mVoice.setVisibility(View.GONE);mLable.setVisibility(View.VISIBLE);mIcon.setImageResource(R.drawable.cancel);mLable.setText("松开手指,取消发送");}}//录音时间太短public void tooShort() {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("录音时间过短");}}//关闭dialogpublic void dimissDialog(){if (mDialog != null&&mDialog.isShowing()) {mDialog.dismiss();mDialog = null;}}/*** 通过level更新voice上的图片** @param level*/public void updateVoiceLevel(int level){if (mDialog != null&&mDialog.isShowing()) {int resId = mContext.getResources().getIdentifier("v" + level, "drawable", mContext.getPackageName());mVoice.setImageResource(resId);}}
}
播放录音类:
/*** Created by Administrator on 2017/11/28.* 播放录音类*/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();//播放错误 防止崩溃mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {mMediaPlayer.reset();return false;}});}else{mMediaPlayer.reset();}try {mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mMediaPlayer.setOnCompletionListener(onCompletionListener);mMediaPlayer.setDataSource(filePath);mMediaPlayer.prepare();mMediaPlayer.start();} catch (IOException e) {e.printStackTrace();}}/*** 如果 播放时间过长,如30秒* 用户突然来电话了,则需要暂停*/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;}}/*** activity 被销毁 释放*/public static void release(){if (mMediaPlayer != null) {mMediaPlayer.release();mMediaPlayer = null;}}
}
listview 的Adapter:
/***list的adapter* Created by Administrator on 2017/11/28.*/public class RecorderAdapter extends ArrayAdapter<Recorder> {//item 最小最大值private int mMinItemWidth;private int mMaxIItemWidth;private LayoutInflater mInflater;public RecorderAdapter(@NonNull Context context, List<Recorder> datas) {super(context,-1, datas);mInflater = LayoutInflater.from(context);//获取屏幕宽度WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics outMetrics = new DisplayMetrics();wm.getDefaultDisplay().getMetrics(outMetrics);//item 设定最小最大值mMaxIItemWidth = (int) (outMetrics.widthPixels * 0.7f);mMinItemWidth = (int) (outMetrics.widthPixels * 0.15f);}@NonNull@Overridepublic View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {ViewHolder holder = null;if (convertView == null) {convertView = mInflater.inflate(R.layout.item_recorder, parent, false);holder = new ViewHolder();holder.seconds = convertView.findViewById(R.id.id_recorder_time);holder.length = convertView.findViewById(R.id.id_recorder_length);convertView.setTag(holder);}else {holder = (ViewHolder) convertView.getTag();}//设置时间 matt.round 四舍五入holder.seconds.setText(Math.round(getItem(position).time)+"\"");//设置背景的宽度ViewGroup.LayoutParams lp = holder.length.getLayoutParams();//getItem(position).timelp.width = (int) (mMinItemWidth + (mMaxIItemWidth / 60f*getItem(position).time));return convertView;}private class ViewHolder{TextView seconds;View length;}
}
在string.xml中
<string name="str_recorder_normal">按住说话</string><string name="str_recorder_recording">松开结束</string><string name="str_recorder_want_cancel">松开手指,取消发送</string>
在style.xml中
<style name="Theme_AudioDialog"><item name="android:windowBackground">@android:color/transparent</item><!--Dialog的windowFrame框为无--><item name="android:windowFrame">@null</item><!--是否浮现在activity之上--><item name="android:windowIsFloating">true</item><!--//是否半透明--><item name="android:windowIsTranslucent">true</item><!--/背景是否模糊显示--><item name="android:backgroundDimEnabled">true</item></style>
好了以上就是所有的代码了。
不懂得可以问我哦,或者自己查查百度吧。
我的注释已经很多了,理理逻辑,应该很好懂的。
源码地址:源码
图片资源:资源图片