我尽量不打错别字,用词准确,不造成阅读障碍。
本文是仿照微信录制语音信息并发送的自定义Button,学习自慕客网上鸿神的视频,视频很老了,但是最近在整理自定义View文章,感觉这个最简单,所以先写出来。
我们有三个类:AudioRecorderButton 、AudioManager 、AudioDialogManager;
分别用来做自定义Button、麦克风管理、dialog管理;
效果图:
我用的模拟器,麦克风接受声音不灵敏,所以等级没显示出来,真机使用是没问题的。
AudioManager
先看AudioManager,因为这个类很独立,不需要访问其他两个类中的方法,只会被访问。这个类主要是用来做麦克风初始化参数和控制,包括打开、关闭、数据源、输出文件、编码格式、计算声音大小等等的设置;
public class AudioManager implements MediaRecorder.OnErrorListener{
private MediaRecorder mMediaRecorder;
private String mCurrentFilePath; //最终的文件保存路径
private boolean isPrepare;
private static AudioManager mInstance;
private AudioManager() {}
public String getCurrentFilePath() {
return mCurrentFilePath;
}
//当初出现一些系统错误的时候会回调
@Override
public void onError(MediaRecorder mediaRecorder, int i, int i1) {
try {
if (mediaRecorder != null)
mediaRecorder.reset();
} catch (Exception e) {
Log.w("AudioManager", "stopRecord", e);
}
}
//回调准备完毕
public interface AudioStateListener {
void wellPrepared();
}
private AudioStateListener mListener;
public void setOnAudioStateListener(AudioStateListener listener) {
mListener = listener;
}
//单例模式获取实例对象
public static AudioManager getInstance() {
if (mInstance == null) {
synchronized (AudioManager.class) {
if (mInstance == null) {
mInstance = new AudioManager();
}
}
}
return mInstance;
}
//对外暴露prepare方法,AudioRecorderButton会调用
public void prepareAudio() {
try {
isPrepare = false;
//为防止文件夹不存在而采取的判断,可根据实际情况删减
File dir = new File(Constants.ChatFileSaveVoice);
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); //设置声音源为麦克风
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR); //设置输出格式
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //设置编码格式
mMediaRecorder.prepare(); //准备
mMediaRecorder.start(); //开始
isPrepare = true; //准备结束
if (mListener != null) {
mListener.wellPrepared(); //通知外界已准备完毕
}
} catch (IOException e) {
e.printStackTrace();
}
}
//随机生成文件名称
private String generateFileName() {
return System.currentTimeMillis() + ".amr";
}
//根据maxLevel生成相应的声音等级
public int getVoiceLevel(int maxLevel) {
if (isPrepare && mMediaRecorder != null) {
try {
//mMediaRecorder.getMaxAmplitude()的范围是0~32767
return maxLevel* mMediaRecorder.getMaxAmplitude() / 32768 + 1;
} catch (IllegalStateException e) {
}
}
return 1;
}
//当手指抬起时我们要停止录音并回收对象,AudioRecorderButton会调用
public void release() {
try {
mMediaRecorder.stop();
mMediaRecorder.release();
mMediaRecorder = null;
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
//当用户取消发送的时候,除了release外还需要删除文件,AudioRecorderButton会调用
public void cancel() {
release();
if (mCurrentFilePath!=null) {
File file = new File(mCurrentFilePath);
file.delete();
mCurrentFilePath = null;
}
}
}
我比较喜欢把原理在注释里写明白,所以请看看注释。
return maxLevel*mMediaRecorder.getMaxAmplitude() / 32768 + 1; 其中mMediaRecorder.getMaxAmplitude()为声音最大振幅,取值范围为[0~32767],如果我们maxLevel为7,那么最大振幅与32768做除法取整数会是[0~1),所以最后结果会是[0~7),+1后就是[1~7]了。没有0这个等级,所以不要用32767做分母。
AudioDialogManager
这个类是用来做dialog显示管理的,也是独立的,没有调用其他两个类;代码很简单;
public class AudioDialogManager {
private Dialog mDialog;
private ImageView mIcon;
private TextView mToShort;
private ImageView mCancel;
private Context mContext;
public AudioDialogManager(Context mContext) {
this.mContext = mContext;
}
//显示dialog,AudioRecorderButton会调用
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 = (ImageView) mDialog.findViewById(R.id.id_recorder_dialog_icon);
mToShort = (TextView) mDialog.findViewById(R.id.id_recorder_dialog_toshort);
mCancel = (ImageView) mDialog.findViewById(R.id.id_recorder_dialog_cancel);
mDialog.show();
}
//录音中时显示等级,AudioRecorderButton会调用
public void recording() {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setVisibility(View.VISIBLE);
mToShort.setVisibility(View.GONE);
mCancel.setVisibility(View.GONE);
mIcon.setImageResource(R.drawable.voice_message1_13x);
}
}
//打算取消时显示取消,AudioRecorderButton会调用
public void wantToCancel() {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setVisibility(View.GONE);
mToShort.setVisibility(View.GONE);
mCancel.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.voice_message23x);
}
}
//录音时间太短时显示,AudioRecorderButton会调用
public void tooShort() {
if (mDialog != null && mDialog.isShowing()) {
mIcon.setVisibility(View.GONE);
mToShort.setVisibility(View.VISIBLE);
mCancel.setVisibility(View.GONE);
mIcon.setImageResource(R.drawable.voice_to_short);
}
}
//关闭dialog,AudioRecorderButton会调用
public void dimissDialog() {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
mDialog = null;
}
}
//通过level去更新Voice的图片,AudioRecorderButton会调用
public void updateVoiceLevel(int level) {
if (mDialog != null && mDialog.isShowing()) {
//准备几张图片
int resId = mContext.getResources().getIdentifier("voice_message1_"+level+"3x", "drawable", mContext.getPackageName());
mIcon.setImageResource(resId);
}
}
}
AudioRecorderButton
重头戏是这个类,代码不短,也不太长。
public class AudioRecorderButton extends android.support.v7.widget.AppCompatButton implements AudioManager.AudioStateListener {
private static final int DISTANCE_Y_CANCEL = 50; //规定坐标绝对值超过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 AudioDialogManager mDialogManager;
private AudioManager mAudioManager;
private float mTime;
private boolean mReady; //是否触发longClick
private AlertDialog alertDialog;
private Dialog showToast = null; //弹出窗口
public AudioRecorderButton(Context context) {
super(context, null);
}
//初始化Button时获取dialog和AudioManager对象等操作
public AudioRecorderButton(Context context, AttributeSet attrs) {
super(context, attrs);
mDialogManager = new AudioDialogManager(getContext());
mAudioManager = AudioManager.getInstance();
mAudioManager.setOnAudioStateListener(this);
setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mReady = true;
mAudioManager.prepareAudio();
return false;
}
});
}
//录音完成后的回调
public interface AudioFinishRecorderListener {
void onFinish(float seconds, String filePath);
}
private AudioFinishRecorderListener mListener;
public void setAudioFinishRecordListener(AudioFinishRecorderListener listener) {
mListener = listener;
}
//监听手势变化
@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) {
if (wantToCancel(x, y)) {
changeState(STATE_WANT_TO_CANCEL);
} else {
changeState(STATE_RECORDING);
}
}
break;
case MotionEvent.ACTION_UP:
if (!mReady) {
reset();
return super.onTouchEvent(event);
}
if (!isRecording || mTime < 0.6f) {
mDialogManager.tooShort();
mAudioManager.cancel();
mHandler.sendEmptyMessageDelayed(MSG_DIALOG_MISS, 1300);
} else if (mCurState == STATE_RECORDING) {
mDialogManager.dimissDialog();
mAudioManager.release();
if (mListener != null) {
mListener.onFinish(mTime, mAudioManager.getCurrentFilePath());
}
} else if (mCurState == STATE_WANT_TO_CANCEL) {
mDialogManager.dimissDialog();
mAudioManager.cancel();
}
reset();
break;
}
return super.onTouchEvent(event);
}
//获取音量大小
private Runnable mGetVoiceLevelRunnable = new Runnable() {
@Override
public void run() {
while (isRecording) {
try {
Thread.sleep(100);
mTime += 0.1f;
if (mTime <= 30f) {
if (mTime >= 25f) {
if (mHandler != null) {
Message msg = mHandler.obtainMessage();
msg.what = HANDLER_REC_TIME;
float i = 30f - mTime;
msg.arg1 = (int) i;
mHandler.sendMessage(msg);
}
}
mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);
}
if (mTime > 31f) {
if (mHandler != null) {
Message stopRecMsg = mHandler.obtainMessage();
stopRecMsg.what = HANDLER_STOP_REC;
mHandler.sendMessage(stopRecMsg);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//创建toast
@SuppressLint("NewApi")
private Dialog getToastDialog() {
try {
String s = android.os.Build.FINGERPRINT.toUpperCase();
if (!s.contains("MIUI")) {
if (alertDialog == null) {
alertDialog = new AlertDialog.Builder(getContext(), R.style.CustomProgressDialog).create();
alertDialog.getWindow().setGravity(Gravity.TOP);// 设置位置
WindowManager.LayoutParams lp = alertDialog.getWindow().getAttributes();
lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
alertDialog.getWindow().setAttributes(lp);
}
return alertDialog;
} else {
if (showToast == null) {
showToast = new Dialog(getContext());
showToast.getWindow().setGravity(Gravity.TOP);// 设置位置
WindowManager.LayoutParams lp = showToast.getWindow().getAttributes();
lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
showToast.getWindow().setAttributes(lp);
}
return showToast;
}
} catch (Exception e) {
Log.e("AudioButton", "" + e.toString());
}
return null;
}
private static final int MSG_AUDIO_PREPARED = 0X110;
private static final int MSG_VOICE_CHANGED = 0X111;
private static final int MSG_DIALOG_MISS = 0X112;
private static final int HANDLER_REC_TIME = 0X2010; // 录音时间计时
private static final int HANDLER_STOP_REC = 0X2011; // 停止录音
//其实应该避免在自定义view中使用Handler
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_AUDIO_PREPARED:
mDialogManager.showRecordingDialog();
isRecording = true;
new Thread(mGetVoiceLevelRunnable).start();
break;
case MSG_VOICE_CHANGED:
mDialogManager.updateVoiceLevel(mAudioManager.getVoiceLevel(7));
break;
case MSG_DIALOG_MISS:
mDialogManager.dimissDialog();
break;
case HANDLER_REC_TIME:
if (!getToastDialog().isShowing() && isRecording) {
getToastDialog().setTitle("将在" + msg.arg1 + "秒后结束录音");
getToastDialog().show();
}
getToastDialog().setTitle("将在" + msg.arg1 + "秒后结束录音");
break;
case HANDLER_STOP_REC:
if (!mReady) {
reset();
break;
}
if (mCurState == STATE_RECORDING) {
mDialogManager.dimissDialog();
if (getToastDialog().isShowing() && isRecording) {
getToastDialog().dismiss();
}
mAudioManager.release();
if (mListener != null) {
mListener.onFinish(mTime, mAudioManager.getCurrentFilePath());
}
}
reset();
break;
}
}
};
//回复状态及标识位
private void reset() {
isRecording = 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;
}
//改变dialog及文字显示
private void changeState(int stateRecording) {
if (mCurState != stateRecording) {
mCurState = stateRecording;
switch (stateRecording) {
case STATE_NORMAL:
setText(R.string.str_recorder_normal);
break;
case STATE_RECORDING:
setText(R.string.str_recorder_recording);
if (isRecording) {
mDialogManager.recording();
}
break;
case STATE_WANT_TO_CANCEL:
setText(R.string.str_recorder_want_cancel);
mDialogManager.wantToCancel();
break;
}
}
}
@Override
public void wellPrepared() {
mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
}
}
这是很久前写的代码,if…else用的比较多,意思明白了就好。
集合了一些简单自定义View的github地址:
https://github.com/longlong-2l/MySelfViewDemo
很简单,没有太多高深的用法,适合学习入门。