最近项目使用录音功能,特此记录下。以及指出小左遇到的问题。
使用MediaRecorder(录音)、MediaPlayer(播放)、ExecutorService(线程池)实现功能。
一:权限:
<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"/>
二:AudioRecoderUtils录音及播放工具
/**
* 录音工具
*/
public class AudioRecoderUtils {
private static ExecutorService mExecutorService = newSingleThreadExecutor();
private boolean isPlaying;//播放状态
private MediaPlayer mMediaPlayer;//播放控件
public String filePath;
public String fileName;
private MediaRecorder mMediaRecorder;
private final String TAG = "MediaRecord";
public static final int MAX_LENGTH = 1000 * 60 * 10;// 最大录音时长1000*60*10;
public static boolean isStart = false;
private OnAudioStatusUpdateListener audioStatusUpdateListener;
public AudioRecoderUtils(){
}
private long startTime;
private long endTime;
/**
* 开始录音 使用amr格式
* 录音文件
* @return
*/
public void startRecord() {
if(!Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)){//存储媒体已经挂载,并且挂载点可读/写。
Log.e(TAG, "SD is error!");
return ;
}
fileName = System.currentTimeMillis()+".amr";
//开始录音创建新文件路径
this.filePath = Environment.getExternalStorageDirectory() + "/" + fileName;
File file = new File(this.filePath);
//文件若存在删除,防止录音覆盖问题
if (!file.exists()){
try {
file.createNewFile();//创建新文件
} catch (IOException e) {
e.printStackTrace();
}
}
// 开始录音
/* ①Initial:实例化MediaRecorder对象 */
if (mMediaRecorder == null) {
mMediaRecorder = new MediaRecorder();
}else{
mMediaRecorder.stop();
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = new MediaRecorder();
}
try {
/* ②setAudioSource/setVedioSource */
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置麦克风
/* ②设置音频文件的编码:AAC/AMR_NB/AMR_MB/Default 声音的(波形)的采样 */
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
/*
* ②设置输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
* ,H263视频/ARM音频编码)、MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB)
*/
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
/* ③准备 */
mMediaRecorder.setOutputFile(filePath);
mMediaRecorder.setMaxDuration(MAX_LENGTH);
mMediaRecorder.prepare();
/* ④开始 */
mMediaRecorder.start();
isStart = true;
// AudioRecord audioRecord.
/* 获取开始时间* */
startTime = System.currentTimeMillis();
updateMicStatus();
Log.i("ACTION_START", "startTime" + startTime);
} catch (IllegalStateException e) {
Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
} catch (IOException e) {
Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
}
}
/**
* 停止(取消)录音
*/
public long stopRecord() {
if(AudioRecoderUtils.isStart) {
AudioRecoderUtils.isStart = false;
if (mMediaRecorder == null)
return 0L;
endTime = System.currentTimeMillis();
Log.i("ACTION_END", "endTime" + endTime);
//有一些网友反应在5.0以上在调用stop的时候会报错,翻阅了一下谷歌文档发现上面确实写的有可能会报错的情况,捕获异常清理一下就行了,!
try {
mMediaRecorder.stop();
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null;
}catch (RuntimeException e){
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null;
e.printStackTrace();
}
return endTime - startTime;
}
return 0L;
}
private final Handler mHandler = new Handler();
private Runnable mUpdateMicStatusTimer = new Runnable() {
public void run() {
updateMicStatus();
}
};
/**
* 更新话筒状态
*/
private int BASE = 1;
private int SPACE = 100;// 间隔取样时间
public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
this.audioStatusUpdateListener = audioStatusUpdateListener;
}
private void updateMicStatus() {
if (mMediaRecorder != null) {
double ratio = (double)mMediaRecorder.getMaxAmplitude() / BASE;
double db = 0;// 分贝
if (ratio > 1) {
db = 20 * Math.log10(ratio);
if(null != audioStatusUpdateListener) {
audioStatusUpdateListener.onUpdate(db);
}
}
mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
}
}
public interface OnAudioStatusUpdateListener {
public void onUpdate(double db);
}
//播放音乐
public boolean playAudio(){
if (null != filePath && !isPlaying) {
isPlaying = true;
mExecutorService.submit(new Runnable() {
@Override
public void run() {
startPlay();
}
});
return true;
}
return false;
}
private void startPlay(){
try {
//初始化播放器
mMediaPlayer = new MediaPlayer();
//设置播放音频数据文件
mMediaPlayer.setDataSource(filePath);
//设置播放监听事件
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
//播放完成
playEndOrFail(true);
}
});
//播放发生错误监听事件
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mediaPlayer, int i, int i1) {
playEndOrFail(false);
return true;
}
});
//播放器音量配置
mMediaPlayer.setVolume(1, 1);
//是否循环播放
mMediaPlayer.setLooping(false);
//准备及播放
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
//播放失败正理
playEndOrFail(false);
}
}
/**
* @description 停止播放或播放失败处理
* @author ldm
* @time 2017/2/9 16:58
*/
private void playEndOrFail(boolean isEnd) {
isPlaying = false;
//TODO isEnd 用于播放状态提示(使用Handler)
if (null != mMediaPlayer) {
mMediaPlayer.setOnCompletionListener(null);
mMediaPlayer.setOnErrorListener(null);
mMediaPlayer.stop();
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
}
三、activity_main.xml布局
<?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="match_parent">
<LinearLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_weight="2">
<TextView
android:id="@+id/tv_recording_content"
android:layout_width="0dip"
android:layout_height="match_parent"
android:maxLines="1"
android:layout_marginRight="5dip"
android:layout_weight="2"/>
<TextView
android:id="@+id/start_recording"
android:background="@drawable/selector_recodering_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dip"
android:paddingLeft="5dip"
android:paddingRight="5dip"
android:text="@string/start_recording"
android:gravity="center"
android:maxLines="1"/>
</LinearLayout>
</RelativeLayout>
四、MainActivity.java调用录音功能
public class MainActivity extends Activity implements View.OnTouchListener, AudioRecoderUtils.OnAudioStatusUpdateListener, View.OnClickListener{
TextView mStartRecording;//开始录音
TextView mRecordingContent;//录音内容
AudioRecoderDialog recoderDialog;//录音弹框
AudioRecoderUtils recoderUtils;//录音工具
private long downT;//按下时间
private static final long RECORD_SHORT_TIMES = 2000L;//录音最少2秒
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initEvent();
initAudioRecoder();
}
private void initView(){
mStartRecording = findViewById(R.id.start_recording);
mRecordingContent = findViewById(R.id.tv_recording_content);
}
private void initEvent(){
mRecordingContent.setOnClickListener(this);
}
private void initAudioRecoder() {
recoderDialog = new AudioRecoderDialog(this);
recoderDialog.setShowAlpha(0.85f);
recoderUtils = new AudioRecoderUtils();
recoderUtils.setOnAudioStatusUpdateListener(this);
mStartRecording.setOnTouchListener(this);
}
@Override
public void onClick(View v) {
switch (view.getId()){//播放
case R.id.tv_recording_content:
String content = mRecordingContent.getText().toString().trim();
if(content!=null&& content.length()>0){
recoderUtils.playAudio();
}
break;
default:
break;
}
}
/**
* 录音
* @param view
* @param event
* @return
*/
@Override
public boolean onTouch(final View view, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN://开始录音
//TODO 动态获取录音权限android.permission.RECORD_AUDIO(没有动态获取权限。Android5.0以后会出现第一次获取权限Bug)
startAudioRecoder(view);
return true;
case MotionEvent.ACTION_UP://结束录音
stopAudioRecoder();
return true;
}
return false;
}
/**
* 开始录音
* @return
*/
private void startAudioRecoder(View view){
recoderUtils.startRecord();
downT = System.currentTimeMillis();
recoderDialog.showAtLocation(view, Gravity.CENTER, 0, 0);
mStartRecording.setBackgroundResource(R.drawable.shape_recoder_btn_recoding);
mStartRecording.setText(R.string.recording);//录音中
}
/**
* 结束录音
*/
private void stopAudioRecoder() {
if(AudioRecoderUtils.isStart) {
Log.d(TAG, "stop record");
long time = recoderUtils.stopRecord();
recoderDialog.dismiss();
mStartRecording.setBackgroundResource(R.drawable.shape_recoder_btn_normal);
mStartRecording.setText(R.string.start_recording);//开始录音
if (time >= RECORD_SHORT_TIMES) {//至少2秒
mRecordingContent.setText(recoderUtils.fileName);
} else {
mRecordingContent.setText("");
Toast.makeText(this, getResources().getString(R.string.record_short_tips), Toast.LENGTH_LONG).show();
}
}
}
@Override
public void onUpdate(final double db) {
if(null != recoderDialog) {
int level = (int) db;
recoderDialog.setLevel((int)db);
recoderDialog.setTime(System.currentTimeMillis() - downT);
}
}
}
五、弹框AudioRecoderDialog
public class AudioRecoderDialog extends BasePopupWindow {
private ImageView imageView;
private TextView textView;
public AudioRecoderDialog(Context context) {
super(context);
View contentView = LayoutInflater.from(context).inflate(R.layout.layout_recoder_dialog, null);
imageView = (ImageView) contentView.findViewById(android.R.id.progress);
textView = (TextView) contentView.findViewById(android.R.id.text1);
setContentView(contentView);
}
public void setLevel(int level) {
Drawable drawable = imageView.getDrawable();
drawable.setLevel(3000 + 6000 * level / 100);
}
public void setTime(long time) {
textView.setText(getProgressText(time));
}
@Override
public void dismiss() {
super.dismiss();
setTime(0L);
}
/**
* 设置时间格式(00:00)
* @param time
* @return
*/
public static String getProgressText(long time) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date(time));
double minute = calendar.get(Calendar.MINUTE);
double second = calendar.get(Calendar.SECOND);
DecimalFormat format = new DecimalFormat("00");
return format.format(minute) + ":" + format.format(second);
}
}
六、弹框基类:
BasePopupWindow
public class BasePopupWindow extends PopupWindow {
private Context mContext;
private float mShowAlpha = 0.88f;
private Drawable mBackgroundDrawable;
@Override
public int getAnimationStyle() {
return super.getAnimationStyle();
}
public BasePopupWindow(Context context) {
this.mContext = context;
initBasePopupWindow();
}
@Override
public void setOutsideTouchable(boolean touchable) {
super.setOutsideTouchable(touchable);
if(touchable) {
if(mBackgroundDrawable == null) {
mBackgroundDrawable = new ColorDrawable(0x00000000);
}
super.setBackgroundDrawable(mBackgroundDrawable);
} else {
super.setBackgroundDrawable(null);
}
}
@Override
public void setBackgroundDrawable(Drawable background) {
mBackgroundDrawable = background;
setOutsideTouchable(isOutsideTouchable());
}
/**
* 初始化BasePopupWindow的一些信息
* */
private void initBasePopupWindow() {
setAnimationStyle(android.R.style.Animation_Dialog);
setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
setOutsideTouchable(false); //默认设置outside点击无响应
setFocusable(true);
}
@Override
public void setContentView(View contentView) {
if(contentView != null) {
contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
super.setContentView(contentView);
addKeyListener(contentView);
}
}
public Context getContext() {
return mContext;
}
@Override
public void showAtLocation(View parent, int gravity, int x, int y) {
super.showAtLocation(parent, gravity, x, y);
ValueAnimator animator = showAnimator();
if(animator != null) {
animator.start();
}
}
@Override
public void showAsDropDown(View anchor) {
super.showAsDropDown(anchor);
ValueAnimator animator = showAnimator();
if(animator != null) {
animator.start();
}
}
@Override
public void showAsDropDown(View anchor, int xoff, int yoff) {
super.showAsDropDown(anchor, xoff, yoff);
ValueAnimator animator = showAnimator();
if(animator != null) {
animator.start();
}
}
@Override
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
super.showAsDropDown(anchor, xoff, yoff, gravity);
ValueAnimator animator = showAnimator();
if(animator != null) {
animator.start();
}
}
@Override
public void dismiss() {
super.dismiss();
ValueAnimator animator = dismissAnimator();
if(animator != null) {
animator.start();
}
}
public void setShowAlpha(float alpha) {
this.mShowAlpha = alpha;
}
/**
* 窗口显示,窗口背景透明度渐变动画
* */
private ValueAnimator showAnimator() {
if(mShowAlpha != 1.0f) {
ValueAnimator animator = ValueAnimator.ofFloat(1.0f, mShowAlpha);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (float) animation.getAnimatedValue();
setWindowBackgroundAlpha(alpha);
}
});
animator.setDuration(360);
return animator;
} else {
return null;
}
}
/**
* 窗口隐藏,窗口背景透明度渐变动画
* */
private ValueAnimator dismissAnimator() {
if(mShowAlpha != 1.0f) {
ValueAnimator animator = ValueAnimator.ofFloat(mShowAlpha, 1.0f);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (float) animation.getAnimatedValue();
setWindowBackgroundAlpha(alpha);
}
});
animator.setDuration(320);
return animator;
} else {
return null;
}
}
/**
* 为窗体添加outside点击事件
* */
private void addKeyListener(View contentView) {
if(contentView != null) {
contentView.setFocusable(true);
contentView.setFocusableInTouchMode(true);
contentView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View view, int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
dismiss();
return true;
default:
break;
}
return false;
}
});
}
}
/**
* 控制窗口背景的不透明度
* */
private void setWindowBackgroundAlpha(float alpha) {
Window window = ((Activity)getContext()).getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.alpha = alpha;
window.setAttributes(layoutParams);
}
}
七、弹框布局:layout_recoder_dialog
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:gravity="center"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:background="@drawable/shape_window_background"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp" >
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:id="@android:id/progress"
android:src="@drawable/layer_recording_animation" />
<TextView
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:id="@android:id/text1"
android:layout_width="114dp"
android:textColor="#FFFFFF"
android:gravity="center"
android:textSize="16sp"
android:text="00:00" />
</LinearLayout>
</LinearLayout>
八、drawable下相关资源
shape_window_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#553e3c3c" />
<corners android:radius="6dp" />
</shape>
layer_recording_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@android:id/background" android:drawable="@mipmap/icon_microphone_normal" />
<item android:id="@android:id/progress" >
<clip android:drawable="@mipmap/icon_microphone_recoding" android:gravity="bottom" android:clipOrientation="vertical" />
</item>
</layer-list>
selector_recodering_btn.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/shape_recoder_btn_recoding"/>
<item android:state_pressed="false" android:drawable="@drawable/shape_recoder_btn_normal"/>
</selector>
shape_recoder_btn_recoding.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#A9B7B7" />
</shape>
shape_recoder_btn_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#00BB9C" />
</shape>
九、strings.xml资源文件
<resources>
<string name="start_recording">开始录音</string>
<string name="recording">录音中...</string>
<string name="record_short_tips">录音太短,至少时长为2秒!</string>
</resources>
十、图片资源
icon_microphone_normal.png icon_microphone_recoding.png
相关问题总结:
1、点击录音按钮出现崩溃:因为点击事件短暂,MediaRecorder对象未初始化完毕就执行stop(),可通过try...catch捕获异常解决。
2、第一次录音时,出现权限弹框。当点击同意时(手指已经离开屏幕),录音继续进行的Bug。可以通过动态获取权限解决此问题(此处未上传,动态获取权限可以看其他大神文章)。